Scans

Core solar analysis engine. Creates comprehensive solar potential assessments including panel placement, production forecasts, ROI calculations, and battery recommendations. See Payback Period, Self-Sufficiency, CO₂ Savings, Dynamic vs Fixed Tariffs, and Accuracy.


GET/v1/scans

List scans

Returns lightweight scan list items for table display. Includes all statuses (processing, completed, error). Supports JWT or API key authentication.

Query parameters

  • Name
    page
    Type
    number
    Description

    Page number (1-based)

  • Name
    limit
    Type
    number
    Description

    Items per page

  • Name
    organizationId
    Type
    string
    Description

    Organization to list scans for. Defaults to personal organization for JWT auth. Ignored for API key auth.

Response

Request

GET
/v1/scans
curl -G https://api.enhub.nl/v1/scans \
  -H "Authorization: Bearer {token}"

Response

{
  "items": [
    {
      "id": "cml3ucftb0001yqzvr4jgakw5",
      "status": "completed",
      "createdAt": "2026-02-10T12:00:00Z",
      "goal": "cost-savings",
      "progression": 100,
      "hasError": false,
      "errorMessage": "string",
      "formattedAddress": "Keizersgracht 1, Amsterdam",
      "hasSeenReport": false
    }
  ],
  "total": 42,
  "page": 1,
  "limit": 20,
  "totalPages": 3
}

POST/v1/scans

Submit solar scan

Submit a solar scan for optimization.

Required attributes

  • Name
    location
    Type
    LocationDto
  • Name
    yearlyKwh
    Type
    number
    Description

    Total annual electricity consumption of the property in kilowatt-hours (kWh/year). This is the single most important input — it determines how much energy needs to be supplied. You can find this value on your annual energy bill. Typical Dutch households consume 2,500-5,000 kWh/year depending on size and appliances. All-electric homes (heat pump, EV) can reach 8,000-15,000 kWh/year. See Accuracy for how this value affects result quality, and Payback Period for how savings are computed.

    Flow behavior:
    • inputMethod: 'known'required. The value is used to scale the standard load profile.
    • inputMethod: 'studio'ignored and overridden. The annual kWh is derived from the custom profile and this value is recalculated.
  • Name
    inputMethod
    Type
    string
    Description

    How the consumption data was provided. Determines which payload construction flow is used.

    Accepted values:
    • "known" — you provide yearlyKwh from your energy bill. A standard residential load profile is scaled to this value. If you have existing solar panels (existingPv), you can optionally provide yearlyExportKwh (teruglevering) for more accurate gross load reconstruction.
    • "studio" — you provide a customProfileId referencing a previously created studio profile (via POST /custom-profiles). The load profile and annual consumption are derived entirely from the studio state. Fields yearlyKwh, yearlyExportKwh, and loadProfile are not used and must be omitted. See Accuracy for how input method affects result quality. One of: known, studio.

Optional attributes

  • Name
    yearlyExportKwh
    Type
    number
    Description

    Annual grid export (teruglevering) in kWh/year, as shown on the energy bill. Used together with solar simulation to reconstruct hourly gross load from net meter data. Must not exceed yearlyKwh. See Accuracy for how this improves gross load reconstruction.

    Flow behavior:
    • inputMethod: 'known' + existingPvused. Distributes annual export across hours using the PVWatts production shape for more accurate gross load reconstruction.
    • inputMethod: 'known' without existingPvrejected (validation error). You cannot have grid export without solar panels.
    • inputMethod: 'studio'rejected (validation error). Export data comes from the studio profile.
  • Name
    loadProfile
    Type
    string
    Description

    Consumption pattern template to use for the hourly load distribution. The template defines WHEN during the day/year electricity is consumed (the shape), while yearlyKwh defines HOW MUCH is consumed in total. Use "standard" for a typical residential pattern. Defaults to "standard" if omitted.

    Flow behavior:
    • inputMethod: 'known'used. Scales this profile to match yearlyKwh.
    • inputMethod: 'studio'rejected (validation error). The studio profile provides its own hourly shape via customProfileId.
  • Name
    customProfileId
    Type
    string
    Description

    ID of a previously uploaded custom consumption profile to use for this scan. Upload your hourly measurement data via POST /studio-profiles to receive this ID. The profile should contain 8,760 hourly consumption values (one per hour of the year). When provided, this results in a much more accurate analysis based on actual measured data. See Accuracy for rating details.

    Flow behavior:
    • inputMethod: 'studio'required. The entire load profile is derived from this profile.
    • inputMethod: 'known'rejected (validation error). Known consumption uses yearlyKwh + loadProfile instead.
  • Name
    existingPv
    Type
    ExistingPvDto[]
  • Name
    newPv
    Type
    NewPvDto[]
  • Name
    tariff
    Type
    TariffDto
  • Name
    electricBattery
    Type
    ElectricBatteryDto
  • Name
    goal
    Type
    string
    Description

    Optimization goal that determines how the system is sized. "self-sufficiency" — maximize the amount of energy produced and consumed on-site, even if it is not the most cost-effective. The optimizer will try to install the maximum solar capacity and include battery storage to minimize grid dependency. "cost-savings" — find the configuration that minimizes total energy costs over the analysis period. The optimizer balances installation costs against long-term savings. If omitted, "cost-savings" is used as the default. See Payback Period for how goal affects cost assumptions, and Self-Sufficiency for the self-sufficiency metric details.

Query parameters

  • Name
    organizationId
    Type
    string
    Description

    Organization to create scan in. Defaults to personal organization for JWT auth. Ignored for API key auth.

Response

Request

POST
/v1/scans
curl -X POST https://api.enhub.nl/v1/scans \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -d '{
  "location": {
    "lat": 52.3676,
    "lon": 4.9041
  },
  "yearlyKwh": 3500,
  "yearlyExportKwh": 800,
  "loadProfile": "standard",
  "inputMethod": "known",
  "customProfileId": "string",
  "existingPv": [
    {
      "name": "existing_pv_1",
      "panelCount": 10,
      "panelPowerWatts": 400,
      "tiltAngle": 35,
      "orientationAngle": 180,
      "systemLosses": 14
    }
  ],
  "newPv": [
    {
      "name": "new_pv_1",
      "minPanelCount": 0,
      "maxPanelCount": 10,
      "panelPowerWatts": 400,
      "tiltAngle": 35,
      "orientationAngle": 180,
      "systemLosses": 14
    }
  ],
  "tariff": {
    "type": "flat",
    "flatEurPerKwh": 0.28,
    "feedInEurPerKwh": 0.1,
    "taxesIncluded": true
  },
  "electricBattery": {
    "powerRatingKw": 10,
    "capacityKwh": 10,
    "minCapacityKwh": 5,
    "minPowerRatingKw": 5,
    "minimumChargeLevel": 20,
    "roundtripEfficiency": 92,
    "totalCost": 8000
  },
  "goal": "self-sufficiency"
}'

Response

{
  "id": "cml3ucftb0001yqzvr4jgakw5",
  "status": "processing",
  "progression": 45,
  "hasError": false,
  "errorMessage": "Optimization failed due to invalid input",
  "createdAt": "2026-02-27T12:00:00.000Z"
}

GET/v1/scans/:id

Get scan by ID

Returns ScanDto for pending/error scans, FullReportDto for completed (optimal) scans. Supports JWT or API key authentication. Anonymous: 3 requests/min per IP.

Path parameters

  • Name
    id
    Type
    string
    Description

    No description available.

Response

Request

GET
/v1/scans/:id
curl -G https://api.enhub.nl/v1/scans/{id} \
  -H "Authorization: Bearer {token}"

Response

{
  "id": "cml3ucftb0001yqzvr4jgakw5",
  "status": "optimal",
  "createdAt": "2026-02-01T12:00:00Z",
  "latitude": 52.37,
  "longitude": 4.89,
  "goal": "self-sufficiency",
  "tariffType": "flat",
  "hasError": true,
  "overview": {
    "kpi": {
      "yearlyUsageKwh": 3500,
      "selfSufficiency": 0.68,
      "selfSufficiencyBaseline": 0,
      "selfSufficiencyImprovement": 0.18,
      "yearlySavings": 140,
      "returnPeriod": 7.5
    },
    "selfConsumption": {
      "selfConsumption": 62,
      "selfSufficiency": 68,
      "yearlyGridPurchaseKwh": 2100,
      "yearlyFeedInKwh": 620
    },
    "energyFlow": {
      "yearlyUsageKwh": 3500,
      "solarYearlyOutputKwh": 2000,
      "yearlyGridPurchaseKwh": 2100,
      "yearlyFeedInKwh": 620,
      "directlyUsedSolarKwh": 1380,
      "batteryCapacityKwh": 6
    },
    "financialSummary": {
      "firstYearElectricityCost": 800,
      "firstYearSavings": 140,
      "totalOwnershipCost": 15000,
      "totalProjectSavings": 2500
    },
    "lifecycleCostBreakdown": {
      "energyBill": 12000,
      "batteryInvestment": 3600,
      "replacementCost": 1200,
      "solarMaintenanceCost": 500,
      "feedInRevenue": -800,
      "totalCost": 15000
    }
  },
  "solar": {
    "existingSystems": [
      {
        "name": "roof_south_1",
        "type": "existing",
        "roofDirection": "south",
        "installedCapacityKwp": 2.4,
        "panelCount": 6,
        "panelPowerRatedWatts": 400,
        "tiltDeg": 35,
        "azimuthDeg": 180,
        "losses": 0.14,
        "lifetimeYears": 25,
        "yearlyOutputKwh": 2000,
        "capacityFactor": 9.5,
        "specificYieldKwhPerKwp": 833,
        "co2SavedKg": 150
      }
    ],
    "newSystems": [
      {
        "name": "roof_south_1",
        "type": "existing",
        "roofDirection": "south",
        "installedCapacityKwp": 2.4,
        "panelCount": 6,
        "panelPowerRatedWatts": 400,
        "tiltDeg": 35,
        "azimuthDeg": 180,
        "losses": 0.14,
        "lifetimeYears": 25,
        "yearlyOutputKwh": 2000,
        "capacityFactor": 9.5,
        "specificYieldKwhPerKwp": 833,
        "co2SavedKg": 150
      }
    ],
    "totalCapacityKwp": 4.8,
    "totalYearlyOutputKwh": 4000,
    "peakOutputKw": 3.2,
    "directConsumption": 62,
    "toGrid": 15,
    "hasSolarPanelsCurrently": true,
    "wantsNewSolarPanels": true
  },
  "battery": {
    "specs": {
      "powerKw": 3,
      "capacityKwh": 6,
      "roundtripEfficiency": 89.9,
      "minChargeLevel": 20
    },
    "performance": {
      "annualSavings": 0,
      "cyclesPerYear": 210,
      "stateOfHealth": 0,
      "gridChargingEnabled": true
    },
    "costs": {
      "purchaseCost": 3600,
      "replacementCost": 0,
      "systemReturnPeriod": 25.7
    },
    "isFeasible": true,
    "wasRequested": true
  },
  "electricityGrid": {
    "annualEnergySuppliedKwh": 1800,
    "annualEnergySuppliedKwhBau": 2600,
    "peakGridDemandKw": 4.8,
    "peakGridDemandKwBau": 5.6
  },
  "financial": {
    "isPositiveOutcome": true,
    "netPresentValue": 2500,
    "investment": {
      "batteryPurchaseCost": 3600,
      "replacementCostNetPresentValue": 1200,
      "solarMaintenanceCost": 500,
      "totalInvestment": 5000,
      "totalInvestmentAfterIncentives": 4500
    },
    "yearOne": {
      "energyBill": 800,
      "feedInRevenue": 150,
      "maintenanceCosts": 50,
      "savingsVsBaseline": 140
    },
    "lifecycle": {
      "energyBill": 12000,
      "feedInRevenue": 800,
      "totalCost": 15000,
      "savingsVsBaseline": 2500
    },
    "parameters": {
      "evaluationPeriod": 25,
      "discountRate": 5,
      "baselineFirstYearCost": 1200,
      "baselineTotalCost": 20000
    },
    "costComparison": {
      "firstYearBaseline": 1200,
      "firstYearOptimized": 800,
      "projectedBaseline": 20000,
      "projectedOptimized": 15000
    },
    "cashflowSummary": {
      "breakEvenYear": 8,
      "endBalance": 5000,
      "avgAnnualReturn": 8.5
    }
  },
  "sustainability": {
    "energyBalance": {
      "totalConsumptionKwh": 3500,
      "solarProductionKwh": 2000,
      "gridPurchaseKwh": 2100,
      "gridFeedInKwh": 620
    },
    "co2Emissions": {
      "gridCarbonIntensity": 0.4,
      "co2SavedTotalKg": 330,
      "co2FromGridImportKg": 840,
      "co2AvoidedByExportKg": 0
    },
    "selfSufficiency": {
      "renewableShare": 68,
      "selfConsumption": 62,
      "directlyUsedRenewableKwh": 1380,
      "netGridDependencyKwh": 1480
    },
    "equivalents": {
      "treesEquivalent": 15,
      "carKmAvoided": 2750,
      "flightsAvoided": 1.3,
      "projectedCo2Savings": 8.25
    },
    "energyMix": {
      "ownGenerationKwh": 2000,
      "gridPurchaseKwh": 2100,
      "renewableShare": 68
    }
  },
  "input": {
    "location": {
      "lat": 52.3676,
      "lon": 4.9041
    },
    "consumption": {
      "yearlyKwh": 3500,
      "loadProfile": "standard",
      "inputMethod": "known",
      "knowsConsumption": true
    },
    "existingPv": [
      {
        "name": "existing_pv_1",
        "panelCount": 10,
        "panelPowerWatts": 400,
        "tiltAngle": 35,
        "orientationAngle": 180,
        "systemLosses": 14
      }
    ],
    "newPv": [
      {
        "name": "new_pv_1",
        "minPanelCount": 0,
        "maxPanelCount": 10,
        "panelPowerWatts": 400,
        "tiltAngle": 35,
        "orientationAngle": 180,
        "systemLosses": 14
      }
    ],
    "tariff": {
      "type": "flat",
      "flatEurPerKwh": 0.28,
      "taxesIncluded": true
    },
    "battery": {
      "powerRatingKw": 10,
      "capacityKwh": 10,
      "minCapacityKwh": 5,
      "minPowerRatingKw": 5,
      "minimumChargeLevel": 20,
      "roundtripEfficiency": 92,
      "totalCost": 8000
    },
    "goal": "self-sufficiency",
    "outageDurations": [
      4,
      8,
      24
    ],
    "customProfileId": "string"
  },
  "outage": {
    "outageStartHours": [
      0
    ],
    "durations": [
      0
    ],
    "emergencyDemand": [
      0
    ],
    "solarDuringOutage": [
      0
    ],
    "batteryOutputDuringOutage": [
      0
    ],
    "batteryLevelDuringOutage": [
      0
    ],
    "solarToBatteryDuringOutage": [
      0
    ]
  }
}

GET/v1/scans/:id/status

Get scan status

Returns lightweight status info for polling. Use this to check scan progression.

Path parameters

  • Name
    id
    Type
    string
    Description

    No description available.

Response

Request

GET
/v1/scans/:id/status
curl -G https://api.enhub.nl/v1/scans/{id}/status \
  -H "Authorization: Bearer {token}"

Response

{
  "id": "cml3ucftb0001yqzvr4jgakw5",
  "status": "processing",
  "progression": 45,
  "hasError": false,
  "errorMessage": "Optimization failed due to invalid input",
  "createdAt": "2026-02-27T12:00:00.000Z"
}

GET/v1/scans/:id/breakdown

Get breakdown data for a scan

Returns time-series data for visualization. Supports day/week/month/year intervals. Use the fields parameter to filter which properties are included in the response. Supports JWT or API key authentication.

Path parameters

  • Name
    id
    Type
    string
    Description

    No description available.

Query parameters

  • Name
    startDate
    Type
    string
    Description

    Start date (ISO 8601 format)

  • Name
    interval
    Type
    string
    Description

    Aggregation interval (default: day) One of: day, week, month, year.

  • Name
    extended
    Type
    boolean
    Description

    Include full hourly SOC data

  • Name
    fields
    Type
    string
    Description

    Comma-separated list of fields to include in response (e.g., "consumption,solarProduction,batteryLevel"). Available fields: consumption, solarUsedOnsite, solarExported, solarStored, solarWasted, solarProduction, gridUsedOnsite, gridStored, baselineGridConsumption, batteryUsedOnsite, batteryLevel, batteryLevelStats, tariffType, tariffRates, labels, intervalType, startDate, endDate, monthlyEnergyCosts, monthlyDemandCosts, monthlyFixedCosts, yearlyElectricityCost, yearlyCashflows, cumulativeCashflows, monthlyCo2SavingsKg. If omitted, all fields are returned.

Response

Request

GET
/v1/scans/:id/breakdown
curl -G https://api.enhub.nl/v1/scans/{id}/breakdown \
  -H "Authorization: Bearer {token}"

Response

{
  "consumption": [
    0
  ],
  "solarUsedOnsite": [
    0
  ],
  "solarExported": [
    0
  ],
  "solarStored": [
    0
  ],
  "solarWasted": [
    0
  ],
  "solarProduction": [
    0
  ],
  "gridUsedOnsite": [
    0
  ],
  "gridStored": [
    0
  ],
  "baselineGridConsumption": [
    0
  ],
  "batteryUsedOnsite": [
    0
  ],
  "batteryLevel": [
    0
  ],
  "batteryLevelStats": [
    {
      "min": 20,
      "max": 95,
      "average": 65
    }
  ],
  "tariffType": "flat",
  "tariffRates": [
    {
      "min": 20,
      "max": 95,
      "average": 65
    }
  ],
  "labels": [
    "string"
  ],
  "intervalType": "day",
  "startDate": "2026-01-15T00:00:00.000Z",
  "endDate": "2026-01-16T00:00:00.000Z",
  "monthlyEnergyCosts": [
    0
  ],
  "monthlyDemandCosts": [
    0
  ],
  "monthlyFixedCosts": [
    0
  ],
  "yearlyElectricityCost": 800,
  "yearlyCashflows": [
    0
  ],
  "cumulativeCashflows": [
    0
  ],
  "monthlyCo2SavingsKg": [
    0
  ]
}

DELETE/v1/scans/:id

Delete scan

Endpoint for DELETE /v1/scans/{id}.

Path parameters

  • Name
    id
    Type
    string
    Description

    No description available.

Response

Request

DELETE
/v1/scans/:id
curl -X DELETE https://api.enhub.nl/v1/scans/{id} \
  -H "Authorization: Bearer {token}"

Was this page helpful?