solarOS Organization API

Organization API

Update editable Quote builder content

Hourly authoring through v1 or MCP is not supported; create hourly lines in the quote builder or use a saved hourly template. Updates purpose-built builder content families while the Quote is editable: sections and lines, pricing adjustments, solar/system design, financing scenarios, payment schedule milestones, incentives, assumptions, savings, and environmental data. Each supplied array replaces that entire content family; omit a family to leave it unchanged. Each financingScenarios entry must be one of the Quote's offeredFundingSources (see GET): send its financingOptionId with the same type. The saved option's terms are copied and any term you send overrides them. A loan's aprInput must be within the lender's saved APR range and its downPaymentInput at least the lender's minimum, and exactly one entry must have isDefault true: the funding source on the proposal (400 VALIDATION_FAILED). Changed 2026-10: custom terms (an entry with no financingOptionId) and options the Quote does not offer used to be stored and are now refused with 400 FUNDING_SOURCE_NOT_OFFERED; error.details.issues names each entry and why, and nothing is written. To migrate, sell the customer's own loan as Cash, or save the lender as a financing option in solarOS and turn it on for the Quote family: Quotes created after that offer it; GET offeredFundingSources shows whether an existing Quote does (a Quote created in solarOS keeps the list it was created with). Plain Cash with no financingOptionId is accepted only when the organization has no saved Cash option. To keep financing the Quote no longer offers, omit financingScenarios. Replacing sections that contain a managed travel charge returns 409. Frozen travel pricing cannot be edited; use the quote travel controls or duplicate the quote. Battery backupPercent and estimatedBackupHours are compatibility inputs that are not persisted; responses derive them from dispatch configuration and load assumptions. No-op PATCH requests return changed=false without ActivityLog, domain event, or timestamp churn. Material writes reject customer-facing Quote states, validate builder constraints such as solar-only system design, and emit ActivityLog plus quote.content.updated domain events with changedFields for webhook consumers. An accepted Quote returns 409 QUOTE_DESIGN_LOCKED: its design, usage, pricing and calculation are what was sold, and changes after the sale go through a change order on the project. Changed 2026-10: accepted Quotes returned 409 CONFLICT before; other customer-facing states still do. Required scope: quotes:content:write.

/api/v1/quotes/{id}/content

Hourly authoring through v1 or MCP is not supported; create hourly lines in the quote builder or use a saved hourly template. Updates purpose-built builder content families while the Quote is editable: sections and lines, pricing adjustments, solar/system design, financing scenarios, payment schedule milestones, incentives, assumptions, savings, and environmental data. Each supplied array replaces that entire content family; omit a family to leave it unchanged. Each financingScenarios entry must be one of the Quote's offeredFundingSources (see GET): send its financingOptionId with the same type. The saved option's terms are copied and any term you send overrides them. A loan's aprInput must be within the lender's saved APR range and its downPaymentInput at least the lender's minimum, and exactly one entry must have isDefault true: the funding source on the proposal (400 VALIDATION_FAILED). Changed 2026-10: custom terms (an entry with no financingOptionId) and options the Quote does not offer used to be stored and are now refused with 400 FUNDING_SOURCE_NOT_OFFERED; error.details.issues names each entry and why, and nothing is written. To migrate, sell the customer's own loan as Cash, or save the lender as a financing option in solarOS and turn it on for the Quote family: Quotes created after that offer it; GET offeredFundingSources shows whether an existing Quote does (a Quote created in solarOS keeps the list it was created with). Plain Cash with no financingOptionId is accepted only when the organization has no saved Cash option. To keep financing the Quote no longer offers, omit financingScenarios. Replacing sections that contain a managed travel charge returns 409. Frozen travel pricing cannot be edited; use the quote travel controls or duplicate the quote. Battery backupPercent and estimatedBackupHours are compatibility inputs that are not persisted; responses derive them from dispatch configuration and load assumptions. No-op PATCH requests return changed=false without ActivityLog, domain event, or timestamp churn. Material writes reject customer-facing Quote states, validate builder constraints such as solar-only system design, and emit ActivityLog plus quote.content.updated domain events with changedFields for webhook consumers. An accepted Quote returns 409 QUOTE_DESIGN_LOCKED: its design, usage, pricing and calculation are what was sold, and changes after the sale go through a change order on the project. Changed 2026-10: accepted Quotes returned 409 CONFLICT before; other customer-facing states still do. Required scope: quotes:content:write.

Update editable Quote builder content. Use an organization API key with quotes:content:write. Check the response envelope and meta.requestId before triggering dependent workflow steps.

Required scopes

quotes:content:write

Parameters

NameInTypeDescription
idrequiredpathstringQuote identifier returned by the Organization API.

Request body

objectrequired
FieldTypeDescription
sections[]array<object>Replaces all sections. Returns 409 when a generated travel charge owns a line; omit this family to retain that decision.
sections[].namerequiredstringOptional unless marked required.
sections[].descriptionstring | nullOptional unless marked required.
sections[].sortOrderintegerOptional unless marked required.
sections[].isCollapsedbooleanOptional unless marked required.
sections[].showLineItemsbooleanOptional unless marked required.
sections[].isVisibleToCustomerbooleanOptional unless marked required.
sections[].showLinesToCustomerbooleanOptional unless marked required.
sections[].customerDisplayNamestring | nullOptional unless marked required.
sections[].lines[]array<object>Optional unless marked required.
sections[].lines[].namerequiredstringOptional unless marked required.
sections[].lines[].descriptionstring | nullOptional unless marked required.
sections[].lines[].productIdstring | nullOptional unless marked required.
sections[].lines[].quantitynumberOptional unless marked required.
sections[].lines[].unitstringOptional unless marked required.
sections[].lines[].unitPricenumberOptional unless marked required.
sections[].lines[].unitCostnumber | nullOptional unless marked required.
sections[].lines[].discountTypePERCENTAGE | FIXED | nullOptional unless marked required.
sections[].lines[].discountValuenumber | nullOptional unless marked required.
sections[].lines[].isOptionalbooleanOptional unless marked required.
sections[].lines[].isSelectedbooleanOptional unless marked required.
sections[].lines[].isTaxablebooleanOptional unless marked required.
sections[].lines[].isVisibleToCustomerbooleanOptional unless marked required.
sections[].lines[].showPriceToCustomerbooleanOptional unless marked required.
sections[].lines[].customerDisplayNamestring | nullOptional unless marked required.
sections[].lines[].specsJSON objectOptional unless marked required.
sections[].lines[].templateLineKeystring | nullOptional unless marked required.
pricingobjectOptional unless marked required.
pricing.methodologyppw | cost_plus | package | tieredOptional unless marked required.
pricing.tierIdstring | nullOptional unless marked required.
pricing.pricePerWattInputnumber | nullOptional unless marked required.
pricing.packagePriceInputnumber | nullOptional unless marked required.
pricing.equipmentCostInputnumberOptional unless marked required.
pricing.laborCostInputnumberOptional unless marked required.
pricing.permitFeesInputnumberOptional unless marked required.
pricing.overheadInputnumberOptional unless marked required.
pricing.marginPercentInputnumber | nullOptional unless marked required.
pricing.marginAmountInputnumber | nullOptional unless marked required.
pricing.taxRateInputnumber | nullOptional unless marked required.
pricing.overridesJSON objectOptional unless marked required.
solarobjectOptional unless marked required.
solar.systemSizeKwnumber | nullOptional unless marked required.
solar.annualProductionKwhnumber | nullOptional unless marked required.
solar.panelCountinteger | nullStands only on a Quote with no panels on its arrays. On a Quote that has them, the Quote's next calculation replaces it with the arrays' total.
solar.inverterTypestring | nullOptional unless marked required.
solar.utilityCompanystring | nullOptional unless marked required.
solar.preSolarAnnualBillUsdnumber | nullOptional unless marked required.
solar.preSolarAnnualKwhnumber | nullOptional unless marked required.
solar.postSolarAnnualBillUsdnumber | nullOptional unless marked required.
solar.projectedSavingsYear1number | nullSet by the Quote's calculation from its bill comparison: a value written here is replaced when the Quote is next calculated.
solar.solarOffsetPercentnumber | nullOptional unless marked required.
solar.roofTypeIdstring | nullOptional unless marked required.
solar.orgRoofTypeConfigIdstring | nullOptional unless marked required.
solar.roofAgeYearsinteger | nullOptional unless marked required.
solar.siteNotesstring | nullOptional unless marked required.
solar.siteLatnumber | nullOptional unless marked required.
solar.siteLngnumber | nullOptional unless marked required.
solar.detailsJSON objectOptional unless marked required.
solar.rateScheduleIdstring | nullThe home's utility rate schedule today: the platform's or one of the organization's own. A generation supplier's schedule (one of a Community Choice Aggregator's, which prices generation alone) is refused with 400 VALIDATION_FAILED (details.issues path solar.rateScheduleId): choose the utility's schedule and set the supplier with generationProviderId.
solar.postSolarRateScheduleIdstring | nullThe rate schedule the home moves to with solar; null uses the one today's schedule names. The same rules as rateScheduleId: a generation supplier's schedule is refused with 400 VALIDATION_FAILED (details.issues path solar.postSolarRateScheduleId).
solar.baselineOverrideKwhnumber | nullOptional unless marked required.
solar.baselineTerritorystring | nullThe home's baseline territory, the code printed on its utility bill (PG&E uses a letter, e.g. X). Matched in any case to the territories of the utilities the Quote's rates bill from (today's, after solar, and the one today's moves solar customers to) and stored as the utility spells it. A code they don't have, or any code when none has a baseline table on file, is refused with 400 VALIDATION_FAILED (details.issues path solar.baselineTerritory); a Quote with no rate yet is checked against every table on file, and a value sent back unchanged is kept. Rates that bill a baseline allowance, such as PG&E E-1 and E-TOU-C, need it; null uses the organization's default. Without either, the solar calculation reports MISSING_BASELINE_TERRITORY.
solar.baselineAllElectricbooleanTrue when electric heat is the home's main heat source: it gets the larger all-electric baseline allowance.
solar.generationProviderIdstring | nullThe Community Choice Aggregator the home buys its power from, such as Sonoma Clean Power, when the utility bill names one under Electric Generation Charges. Null, the default, means the utility supplies it. The calculation then bills the utility's delivery charges, the Power Charge Indifference Adjustment (PCIA) and franchise fee surcharge for the home's vintage, and the supplier's generation rates, and credits exports in each one's own way. It needs pciaVintage, and must bill through the utility of the Quote's rates, before and after solar. A supplier the organization can't see, one billing through another utility, or a rate change that would leave it on another utility's rate is refused with 400 VALIDATION_FAILED (details.issues path solar.generationProviderId). Null clears pciaVintage and generationProductKey too.
solar.pciaVintageinteger | nullThe home's PCIA vintage: the year on its PG&E bill beside the Power Charge Indifference Adjustment. It sets that charge and the franchise fee surcharge. Required with generationProviderId; a year the utility has no PCIA rate for, or a year sent with no supplier, is refused with 400 VALIDATION_FAILED (details.issues path solar.pciaVintage).
solar.generationProductKeystring | nullThe supplier's premium product the home buys, such as evergreen for Sonoma Clean Power EverGreen. Null is its standard product. A key the supplier doesn't offer, or one sent with no supplier, is refused with 400 VALIDATION_FAILED (details.issues path solar.generationProductKey). Changing generationProviderId without it drops it.
solar.arrays[]array<object>Replaces all arrays on the Quote. Refused with 409 when an existing design-managed array row was hand-edited in the builder, unless solar.force is true.
solar.arrays[].namestring | nullOptional unless marked required.
solar.arrays[].panelCountrequiredintegerOptional unless marked required.
solar.arrays[].panelWattagerequirednumberOptional unless marked required.
solar.arrays[].azimuthnumber | nullOptional unless marked required.
solar.arrays[].tiltnumber | nullOptional unless marked required.
solar.arrays[].shadingFactornumber | nullOptional unless marked required.
solar.arrays[].soilingFactornumber | nullOptional unless marked required.
solar.arrays[].snowLossFactornumber | nullOptional unless marked required.
solar.arrays[].arraySizeKwnumber | nullOptional unless marked required.
solar.arrays[].annualProductionKwhnumber | nullOptional unless marked required.
solar.arrays[].grossAnnualKwhnumber | nullOptional unless marked required.
solar.arrays[].productionProfileJSON objectOptional unless marked required.
solar.arrays[].panelProductIdstring | nullOptional unless marked required.
solar.arrays[].detailsJSON objectOptional unless marked required.
solar.arrays[].sortOrderintegerOptional unless marked required.
solar.inverters[]array<object>Optional unless marked required.
solar.inverters[].typerequiredstring | micro | hybrid | otherOptional unless marked required.
solar.inverters[].manufacturerstring | nullOptional unless marked required.
solar.inverters[].modelstring | nullOptional unless marked required.
solar.inverters[].quantityintegerOptional unless marked required.
solar.inverters[].powerKwnumber | nullOptional unless marked required.
solar.inverters[].productIdstring | nullOptional unless marked required.
solar.inverters[].sortOrderintegerOptional unless marked required.
solar.batteries[]array<object>Optional unless marked required.
solar.batteries[].manufacturerstring | nullOptional unless marked required.
solar.batteries[].modelstring | nullOptional unless marked required.
solar.batteries[].capacityKwhrequirednumberOptional unless marked required.
solar.batteries[].powerKwnumber | nullOptional unless marked required.
solar.batteries[].quantityintegerOptional unless marked required.
solar.batteries[].backupPercentnumber | nullCompatibility input only. The value is not persisted; responses derive it from the Quote battery dispatch configuration.
solar.batteries[].estimatedBackupHoursnumber | nullCompatibility input only. The value is not persisted; responses calculate runtime from capacity, reserve, and load assumptions.
solar.batteries[].productIdstring | nullOptional unless marked required.
solar.batteries[].detailsJSON objectFree-form row details. details.unitRole is EXPANSION for an expansion unit (absent or MAIN for a main unit). details.onSite true marks the battery already at the home that an expansion-only sale adds to: it counts toward the bank's power, backup hours and expansion limit, but it is not sold — not priced, not on the proposal and not in the sold system.
solar.batteries[].sortOrderintegerOptional unless marked required.
solar.systemDesignobjectOptional unless marked required.
solar.systemDesign.designModeauto | manual | hybridOptional unless marked required.
solar.systemDesign.activePanelsJSON objectRetired Google-layout input. Accepted for compatibility but ignored on writes; reads preserve the stored value. Author panels in customRoofSegments.
solar.systemDesign.obstructionsJSON objectOptional unless marked required.
solar.systemDesign.setbacksJSON objectOptional unless marked required.
solar.systemDesign.targetOffsetPercentnumber | nullOptional unless marked required.
solar.systemDesign.selectedPanelProductstring | nullOptional unless marked required.
solar.systemDesign.totalActivePanelsinteger | nullOptional unless marked required.
solar.systemDesign.designSizeKwnumber | nullOptional unless marked required.
solar.systemDesign.estimatedProductionKwhnumber | nullOptional unless marked required.
solar.systemDesign.customRoofSegmentsJSON objectOptional unless marked required.
solar.systemDesign.canvasImageBoundsJSON objectOptional unless marked required.
solar.forcebooleanConfirms overwriting hand-edited, design-managed array rows when replacing arrays. Without it, such a replacement is refused with 409 HAND_EDITED_ROW. Never stored.
financingScenarios[]array<object>Replaces all financing on the Quote. Each entry must be one of the Quote's offeredFundingSources from GET /quotes/{id}/content, and takes the saved option's terms unless it sends its own; custom terms and options the Quote does not offer return 400 FUNDING_SOURCE_NOT_OFFERED. Exactly one entry has isDefault true.
financingScenarios[].financingOptionIdstring | nullA saved financing option the Quote offers. Null only for plain Cash, when the organization has no saved Cash option.
financingScenarios[].typerequiredCASH | LOAN | LEASE | PPA | PREPAID_TPOMust match the saved financing option type.
financingScenarios[].namerequiredstringOptional unless marked required.
financingScenarios[].dealerFeePercentInputnumber | nullOptional unless marked required.
financingScenarios[].dealerFeeTreatmentInputADD_ON_TOP | ABSORBEDOptional unless marked required.
financingScenarios[].loanTermMonthsInputinteger | nullOptional unless marked required.
financingScenarios[].aprInputnumber | nullA loan's APR, within the lender's saved range.
financingScenarios[].downPaymentInputnumber | nullA loan's cash down, at least the lender's minimum. Starts at that minimum.
financingScenarios[].leaseTermYearsInputinteger | nullOptional unless marked required.
financingScenarios[].leaseEscalatorPercentInputnumber | nullOptional unless marked required.
financingScenarios[].ppaTermYearsInputinteger | nullOptional unless marked required.
financingScenarios[].ppaRatePerKwhInputnumber | nullOptional unless marked required.
financingScenarios[].ppaEscalatorPercentInputnumber | nullOptional unless marked required.
financingScenarios[].prepaidProviderFundingInputnumber | nullA prepaid plan's provider funding. The customer's price comes down by at most the share of it the saved option passes on (a percent of the price), the rest staying the contractor's.
financingScenarios[].prepaidTpoFramingInputPREPAID_PPA | PREPAID_LEASE | nullOptional unless marked required.
financingScenarios[].cashDiscountPercentInputnumber | nullA cash plan's discount, as a percent of the price. On a solar Quote it comes off the Quote's total at its next calculation, with discountAmount and the milestone amounts following.
financingScenarios[].isDefaultbooleanThe funding source on the proposal. Exactly one entry is.
paymentScheduleobjectOptional unless marked required.
paymentSchedule.fundingSources[]array<object>Optional unless marked required.
paymentSchedule.fundingSources[].idstringOptional unless marked required.
paymentSchedule.fundingSources[].namerequiredstringOptional unless marked required.
paymentSchedule.fundingSources[].typecash | financed | otherOptional unless marked required.
paymentSchedule.fundingSources[].inputTypefixed | percent | remainderOptional unless marked required.
paymentSchedule.fundingSources[].inputValuerequirednumberOptional unless marked required.
paymentSchedule.fundingSources[].sortOrderintegerOptional unless marked required.
paymentSchedule.milestones[]array<object>Optional unless marked required.
paymentSchedule.milestones[].namerequiredstringOptional unless marked required.
paymentSchedule.milestones[].typerequiredpercent_of_total | percent_of_source_pool | percent_of_remaining | fixedOptional unless marked required.
paymentSchedule.milestones[].inputValuerequirednumberOptional unless marked required.
paymentSchedule.milestones[].fundingSourceIdstring | nullOptional unless marked required.
paymentSchedule.milestones[].capAmountnumber | nullOptional unless marked required.
paymentSchedule.milestones[].dueUponCategoryDEPOSIT | DEPOSIT_SOLAR | CUSTOMER_PLAN_APPROVAL | SUBSTANTIAL_COMPLETION_OF_SOLAR_WORK | FINAL_SOLAR_PERMIT_RECEIVED | PTO_RECEIVED | OTHER | nullOptional unless marked required.
paymentSchedule.milestones[].dueOnEventstring | nullOptional unless marked required.
paymentSchedule.milestones[].sortOrderintegerOptional unless marked required.
incentives[]array<object>Optional unless marked required.
incentives[].typerequiredFEDERAL_TAX_CREDIT | STATE_TAX_CREDIT | UTILITY_REBATE | STATE_REBATE | SREC | LOCAL_INCENTIVE | OTHEROptional unless marked required.
incentives[].namerequiredstringOptional unless marked required.
incentives[].valueTyperequiredPERCENT | FLATOptional unless marked required.
incentives[].valuerequirednumberOptional unless marked required.
incentives[].amountComputednumberOptional unless marked required.
incentives[].isCustomerFacingbooleanOptional unless marked required.
incentives[].notesstring | nullOptional unless marked required.
assumptionsobjectOptional unless marked required.
assumptions.degradationRatePercentnumberOptional unless marked required.
assumptions.systemLifetimeYearsintegerOptional unless marked required.
assumptions.performanceRationumberOptional unless marked required.
assumptions.utilityEscalationPercentnumber | nullOptional unless marked required.
assumptions.netMeteringPolicystring | nullOptional unless marked required.
assumptions.discountRatePercentnumber | nullOptional unless marked required.
assumptions.inflationRatePercentnumber | nullOptional unless marked required.
assumptions.inverterReplacementYearinteger | nullOptional unless marked required.
assumptions.inverterReplacementCostnumber | nullOptional unless marked required.
assumptions.gridEmissionsFactornumber | nullOptional unless marked required.
assumptions.overridesJSON objectOptional unless marked required.
savingsobjectOptional unless marked required.
savings.annualBillBeforeComputednumberOptional unless marked required.
savings.monthlyBillBeforeComputednumberOptional unless marked required.
savings.annualBillAfterComputednumberOptional unless marked required.
savings.monthlyBillAfterComputednumberOptional unless marked required.
savings.monthlyBillsBeforeComputedJSON objectOptional unless marked required.
savings.monthlyBillsAfterComputedJSON objectOptional unless marked required.
savings.annualSavingsYear1ComputednumberOptional unless marked required.
savings.lifetimeSavingsComputednumberOptional unless marked required.
savings.lifetimeSavingsNetComputednumberOptional unless marked required.
savings.savingsByScenarioComputedJSON objectOptional unless marked required.
savings.paybackYearsComputednumber | nullOptional unless marked required.
savings.roiPercentComputednumber | nullOptional unless marked required.
savings.paybackByScenarioComputedJSON objectOptional unless marked required.
savings.yearlyProjectionComputedJSON objectOptional unless marked required.
savings.assumptionsSnapshotJSON objectOptional unless marked required.
environmentalobjectOptional unless marked required.
environmental.co2OffsetLbsComputednumberOptional unless marked required.
environmental.co2OffsetTonsComputednumberOptional unless marked required.
environmental.treesEquivalentComputednumberOptional unless marked required.
environmental.carsOffRoadComputednumberOptional unless marked required.
environmental.milesDrivenComputednumberOptional unless marked required.
environmental.homesEquivalentComputednumberOptional unless marked required.
environmental.annualCo2OffsetLbsComputednumberOptional unless marked required.
environmental.annualTreesEquivalentComputednumberOptional unless marked required.
environmental.gridEmissionsFactorUsednumber | nullOptional unless marked required.
patchquotebuildercontentRequest
{
  "sections": [
    {
      "name": "Solar installation",
      "lines": [
        {
          "name": "PV installation",
          "quantity": 1,
          "unit": "each",
          "unitPrice": 31500
        }
      ]
    }
  ],
  "solar": {
    "systemSizeKw": 8.4,
    "arrays": [
      {
        "panelCount": 21,
        "panelWattage": 400
      }
    ],
    "batteries": [
      {
        "manufacturer": "Example Energy",
        "capacityKwh": 13.5,
        "quantity": 1
      }
    ],
    "systemDesign": {
      "designMode": "manual",
      "totalActivePanels": 21,
      "designSizeKw": 8.4
    }
  }
}

Responses

200

Updated Quote builder content envelope with changed metadata

#/components/schemas/OrgApiResourceEnvelope

400

Validation or builder constraint error, or FUNDING_SOURCE_NOT_OFFERED when a financingScenarios entry is custom terms or an option the Quote does not offer

#/components/schemas/OrgApiError

401

Missing or invalid organization API key

#/components/schemas/OrgApiError

402

Organization billing access or an endpoint-specific business quota denied the request

#/components/schemas/OrgApiError

403

Missing quotes:content:write scope

#/components/schemas/OrgApiError

404

Quote not found

#/components/schemas/OrgApiError

409

QUOTE_DESIGN_LOCKED when the Quote was accepted; otherwise the Quote requires revision/duplicate flow, managed travel prevents section replacement, or array replacement would overwrite hand-edited design rows (HAND_EDITED_ROW); reconcile edits before explicitly confirming with solar.force

#/components/schemas/OrgApiError

429

Rate limit exceeded

#/components/schemas/OrgApiError

Errors

400 Validation or builder constraint error, or FUNDING_SOURCE_NOT_OFFERED when a financingScenarios entry is custom terms or an option the Quote does not offer401 Missing or invalid organization API key402 Organization billing access or an endpoint-specific business quota denied the request403 Missing quotes:content:write scope404 Quote not found409 QUOTE_DESIGN_LOCKED when the Quote was accepted; otherwise the Quote requires revision/duplicate flow, managed travel prevents section replacement, or array replacement would overwrite hand-edited design rows (HAND_EDITED_ROW); reconcile edits before explicitly confirming with solar.force429 Rate limit exceeded

Static examples

curl -X PATCH "$SOLAROS_BASE_URL/api/v1/quotes/$SOLAROS_QUOTE/content" \
  -H "Authorization: Bearer $SOLAROS_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{"sections":[{"name":"Solar installation","lines":[{"name":"PV installation","quantity":1,"unit":"each","unitPrice":31500}]}],"solar":{"systemSizeKw":8.4,"arrays":[{"panelCount":21,"panelWattage":400}],"batteries":[{"manufacturer":"Example Energy","capacityKwh":13.5,"quantity":1}],"systemDesign":{"designMode":"manual","totalActivePanels":21,"designSizeKw":8.4}}}'