# pvperf > API-first, AI-ready PV performance model: energy yield and the full loss diagram at the weather file's time step (irradiance rows in kWh/m², energy rows in kWh/kWp) from real PAN/OND component files, validated per hour against a reference tool's outputs. ## Instructions - Base URL: https://pvperf.com. Send every call to `https://pvperf.com/v1/…`; the paths below are written that way. The same routes also answer under `/api/…`, the prefix the workbench uses. - Simple run: `POST /v1/run/simple` with `latitude`, `longitude` and `system_capacity_kw` (or `modules` with `module_power_w`) runs the system on a typical year of the location with the PVWatts DC model and a flat-efficiency inverter. - Optional: `module_type`, `racking` (fixed, rooftop, single_axis), `tilt_deg`, `azimuth_deg`, `inverter_ac_kw` or `dc_ac_ratio`, `inverter_efficiency`, `losses_percent`, `timezone`, `weather`, `weather_source`. - The answer adds `request` (a full body for `POST /v1/run`, with the weather by id) and `defaults_applied` (each value assumed, with its source). Guide: https://pvperf.com/v1/docs/simple_run.md. - A typical year alone: `GET /v1/weather/tmy?latitude=…&longitude=…` (free, no key); `&fields=meta` returns its id and provenance without the hourly rows. - Residential roof: one roof plane per run. - `POST /v1/run/simple` with `racking: "rooftop"`, the plane's `tilt_deg` and `azimuth_deg`, and its panel count as `modules` with `module_power_w`. The answer's `request` is already set for a roof: `array.track_mode` rooftop, `array.inter_row_shading` false, no medium-voltage or auxiliary losses, and an inline inverter at a DC/AC ratio of 1.2. - Near shading: add a `scene` to that `request` and send it to `POST /v1/run`. Start from `GET /v1/scene-examples/residential` (a gable house, a neighbouring house, two trees), keep its frame block, put one placement per module on the roof plane (tilt = the roof pitch) and replace the objects with the house and its surroundings. - `array.shade_mode`: `linear` (the default) suits microinverters and optimizers. `electrical` suits string inverters. - Starting from `GET /v1/defaults` (a utility plant) instead: set `array.track_mode` to rooftop and `array.inter_row_shading` to false. Set `ac_losses.use_auxiliaries`, `use_mv_transformer` and `use_mv_line` to false, or the energy to the grid can turn negative. Use a generic inverter (below) or give the inverter inline. - Generic components: for a design without module or inverter files, the library holds generic placeholders. Modules: `generic-module-400w-residential`, `generic-module-440w-residential`, `generic-module-580w-commercial`, `generic-module-620w-bifacial`, `generic-module-700w-bifacial`, `generic-module-540w-cdte`. Inverters: `generic-inverter-micro-350w`, `generic-inverter-1ph-3800w`, `generic-inverter-1ph-7600w`, `generic-inverter-1ph-11400w`, `generic-inverter-3ph-30kw`, `generic-inverter-3ph-60kw`, `generic-inverter-3ph-125kw`, `generic-inverter-string-350kw`, `generic-inverter-central-4400kw`. - Send one as `{"id": "generic-…"}` in `module` or `inverter`. The run warns `generic_component`. Every value and how each entry is made: https://pvperf.com/v1/docs/generic_components. - Microinverters: `array.modules_per_string` 1, `array.strings_per_inverter` 1 and `array.n_inverters` equal to the module count. - One setup per run: a run is one plant, with one module, one inverter design and one racking geometry. For several roof planes, products or row spacings, make one run each and add the results. Keep the whole house and its surroundings as objects in each plane's run, so every plane sees the same shade. A scene whose placements face several ways runs as one plant: its orientations are blended by module count and the result is not split per orientation. - Weather: the simple run without `weather` and `GET /v1/weather/tmy` use a typical year at the location: in the Americas the NSRDB GOES TMY v4 (satellite-derived; pvperf's copy of NSRDB's 2025 edition, the site nearest the point), elsewhere the PVGIS 5.3 TMY. `meta.source` names the year used. - `source` on `GET /v1/weather/tmy` (`weather_source` on the simple run) picks `nsrdb` or `pvgis` instead of `auto`. The PVGIS year's radiation in the Americas is PVGIS-ERA5, a reanalysis, not satellite-derived data. Where NSRDB has no year, `auto` uses PVGIS and `meta.notes` says why. - For NSRDB or measured data, upload the file to `POST /v1/parse-weather` and send the returned `{"id": "met_…"}` as `weather`. - A year sent by id keeps the time base it was stored in; `site.timezone` sets the clock of the hourly answer (the simple run fills in the location's standard time, from its IANA zone). With inline weather, `site.timezone` is the zone of its timestamps. - Full request: `POST /v1/run` with a JSON body of sections `site`, `weather`, `array`, `module`, `inverter` (+ optional `iam`, `thermal`, `dc_losses`, `ac_losses`, `spectral`, `bifacial`, `scene`, `storage`, `options`, `lifetime` (one year only) and `include_series`). Unknown keys are rejected (422 with the field path). Fractions travel as 0–1. - `GET /v1/defaults` is a complete runnable request (about 930 KB, the weather inline); the simple run's `request` is a smaller start, with the weather by id. - Components by reference: `{"id": "pan_…"}` / `{"id": "ond_…"}` from the parse endpoints, or a library slug from `GET /v1/library/modules|inverters?q=…` (every word, in any order). Weather by reference: `{"id": "met_…"}` from `POST /v1/parse-weather`. - The default response is the summary (annual + monthly) and the loss diagram (~3 KB). Every hourly series with `"include_series": true`, or only the ones named: `"include_series": ["p_grid_w"]`. - Every response carries `engine_version`, `run_id`, `status`. Send `Idempotency-Key` to make a simulate call replayable for 24 h. - Scene (3D site, optional): a `scene` section replaces the flat row geometry with typed devices and shading objects. Guide: https://pvperf.com/v1/docs/scene_agents (the conventions, a recipe, converting a CAD, GIS or KML layout, every rejection with its fix). - Devices: `gft` fixed racks and `sat` single-axis trackers in any mix, or `ewf` east-west ridges on their own (set `array.track_mode` to `ewf`; guide: https://pvperf.com/v1/docs/east_west). One placement per device: centre x east, y north, z up; azimuth clockwise from north; tilt; roll. - Objects: `prism`, `wall`, `tree_line`, `cylinder`, and flat `outlines`. Lengths are unitless: one unit of your choice for every length. - On a roof: a gft type placed on the roof plane (tilt = the roof pitch, z = the height of the array centre). Switch row-to-row shading off (`array.inter_row_shading = false`) and leave bifacial out; a building is one or more prisms. Tilted rows on a flat roof keep row-to-row shading on. Objects shade with row-to-row shading on or off. - Tracker rotation limits, backtracking and stow are set in the array section, not per tracker. The keys are `rotation_limit_deg` (or `rotation_min_deg` and `rotation_max_deg`), `backtracking` and `wind_stow`. - The array section stays the plant description (GCR, collector width, stringing); the scene's module counts weight the blend. The stringing and the inverter section set the nameplate: string the scene's module total and size the inverter to it. - Start from `GET /v1/scene-examples/{id}` (residential, commercial, ground_fixed, ground_tracker, east_west; `GET /v1/scene-examples` gives each one's `array_patch` and `notes`), keep the frame block, replace types and placements, check with `POST /v1/scene/validate`, then run. - `validate` checks the schema and ids and returns counts (with the orientation bins the run will use) and the parser's warnings. The origin distance, the pitch check and the module-total comparison need the site and are reported by `POST /v1/run` in `summary.warnings` or as a 422. - A PVsyst variant file's 3D scene converts with `POST /v1/parse-scene` (multipart `file`; the byte-order mark PVsyst writes is accepted): merge the returned `array_patch` into `array` before running; it carries the tracker operation. - After a run, `POST /v1/scene/day?date=MM-DD` with the same request returns one day solved: per timestep the sun, the weather, the field's plane-of-array irradiance and shading, each orientation bin's tracker rotation and each table's beam shaded fraction (not charged). - Field pages: `scene`, `scene.types`, `scene.placements`, `scene.objects`, `scene.outlines`. - Run time: a scene run grows with its placements and the objects that can reach them; on 100 fixed racks each object adds about 0.17 s of compute. Fewer, larger prisms run faster than many small ones: merge neighbouring cells of a surface model that have similar heights into one prism, and model a tree as one cylinder. - Storage: a `storage` section adds a top-level `storage` block (annual balances, monthly throughput, SOC profile) and `storage.*` rows to the loss diagram; the solar results stay storage-free. Field page `storage`. - Keys: `coupling` ac | dc, `power_kw`, `capacity_kwh`, `dispatch` clip | force_cycle | timed, `dispatch_schedule` (24 hour flags), `interconnect_limit_kw`, `round_trip_efficiency`, … - AC coupling works on the plant's grid injection and needs `interconnect_limit_kw` for clip and force_cycle. - DC coupling sits ahead of the PV inverter. It captures the DC power the inverter cannot take at its AC ceiling or its input-current limit (`e_clip_captured_dc_kwh`) and delivers through the inverter (`e_discharge_dc_kwh`); its target defaults to the plant AC nameplate. Where the current limit binds, holding the array nearer its maximum-power point lowers the inverter's input in that hour, reported as `e_inverter_input_given_up_dc_kwh`. - For both couplings `e_charge_kwh` and `e_discharge_kwh` are the grid injection given up and added (each summary energy is the sum of the series of the same name). - `timed` needs a window (at least one flag set, at least one clear): it charges outside the window and delivers a flat power inside it. - A starting size: power 0.5 × the plant AC nameplate (`array.n_inverters` × `inverter.p_nom_ac_w`, reported as `summary.p_nom_ac_kw`) to two significant figures, capacity 4 h × power, target 0.8 × the AC nameplate unrounded. - Authentication: send `Authorization: Bearer pvp_live_…`. Sign in to the workbench at https://pvperf.com/ with your email and create a key on the Account page. `GET /v1/me` shows the account, the free units left this month, the balance and the unit price. - Where the service requires an account, a run or model-tree request without a key is `401 unauthorized`, and an invalid or revoked key is always 401. - The default project is free and needs no key: the request of `GET /v1/defaults` (without `_meta`) with any Model-tab method choices and no other change. Its response carries `"default_project": true` and no `billing`, and it is not stored as a past run. Any other change to the request makes it a charged run. - A run that the month's free units, the wallet or the monthly budget cannot cover is `402 payment_required` with a `remedy` (`top_up`, `raise_budget` or `update_card`). The problem carries the run's `units`, `reasons` and `price_usd`. - A charged run's response carries a `billing` block: `units`, `reasons`, `unit_price_usd`, `price_usd`, `paid_by`, `charged_usd`, `free_units_left`, and `balance_usd` or `month_to_date_usd`. A run that answers 202 carries the same block. An idempotent replay is not charged again. - Pricing: A run takes `units = (1 + ⌊placements / 100⌋ + ⌊shading objects / 50⌋) × terrain-aware multiplier × time-step multiplier`. `GET /v1/pricing` gives the rules and the unit price. - Placements are the scene's racks, trackers or roof modules. Shading objects are its prisms, walls, tree lines and cylinders. A run without a scene has neither. - The terrain-aware multiplier is 2 for a run with terrain-aware backtracking (`array.backtracking_angle` `terrain_aware`, trackers in the scene), else 1. - The time-step multiplier is 1 for 60-minute weather, 2 for 30-minute weather, 4 for 15-minute weather and 6 for 10-minute weather. - Weather longer than a year counts once per year. - A unit costs USD 0.001. Each account gets 25 free units a month. A run is paid from the free units when they cover all of its units. - A charged run's `billing.reasons` lists each term, for example `base 1`, `1,976 placements +19`, `terrain-aware backtracking ×2` and `15-minute weather ×4`. - Errors are RFC 9457 `application/problem+json` with an enumerated `code` and the offending `field`. - A run that fails is not charged: its error says why, ends with "Nothing was charged." and carries `charged: false` (REST and MCP alike). Every run's answer carries `charged`. An answer ready more than 50 s after the run started (the gateway waits 60 s) is stored, not charged, and says so in its warnings. The gateway's own 504 (no answer within 60 s) and 502 (the function stopped) are problem documents that say the same. - Warnings never block a run: `summary.warnings` is a list of `{code, field, message}`. Codes: - `string_voc_above_vdc_max`, `string_vmp_above_mppt_max` and `string_vmp_below_mppt_min`: the string-voltage check on `array.modules_per_string`, with the design low temperature `site.design_t_min_c`, else the weather's lowest ambient temperature. - `dc_ac_ratio`: the DC/AC ratio is outside 0.8–2. - `scene_pitch` and `scene_modules`: scene geometry. - `snow_not_modelled`: the Townsend snow loss asked for on east-west racking. - `module_model_pvwatts`: a module sent with some electrical fields and no model, run on the PVWatts DC model; the message names the missing fields. - `generic_component`: the module or inverter is a generic placeholder from the library (ids starting `generic-`), sent by id or with its values inline. - `note`: any other remark of the engine. - Stored runs: every run made with an account is kept 365 days. `POST /v1/run` then returns `links.run` and `links.series_csv`; `GET /v1/runs` lists your runs (newest first, `before` pages back), `GET /v1/runs/{id}` returns the request and the response (add `include_series=true` for the hourly series) and `GET /v1/runs/{id}/series.csv` the hourly series as CSV. Reading is free. - MCP: `https://pvperf.com/mcp` (streamable HTTP) with eleven tools: get_defaults, simulate, simulate_simple, get_typical_weather, get_run, list_runs, search_modules, search_inverters, add_component_file, validate_scene, send_feedback. Send the API key as `Authorization: Bearer pvp_live_…`; simulate and simulate_simple are charged like `POST /v1/run` (the default project is free) and the other tools are free. - Feedback: `POST /v1/feedback` with `kind` (bug or feedback), `text`, and optionally `request` (the run JSON you had trouble with), `run_id` (one of your stored runs), `contact` (an email for a reply). No key needed. The MCP tool `send_feedback` does the same. Patch notes: `GET /v1/patch-notes`. Terms and Conditions: `GET /v1/terms`. - Demo runs, free and without an account: `GET /v1/demos` lists them; `GET /v1/demos/{id}` returns a complete request with its stored result (hourly series included); `POST /v1/demos/{id}/day?date=MM-DD` solves one day of a scene demo. ## Minimal example ``` curl -X POST https://pvperf.com/v1/run/simple -H "Authorization: Bearer pvp_live_…" -H "Content-Type: application/json" -d '{"latitude": 39.8, "longitude": -89.85, "system_capacity_kw": 8}' ``` Replace `pvp_live_…` with your API key. The answer's `summary.e_grid_kwh` is the energy to the grid for a typical year. Edit `request` and send it to `POST https://pvperf.com/v1/run` to change anything else. ## Canonical example ``` curl -X POST https://pvperf.com/v1/run -H "Content-Type: application/json" --data-binary @request.json ``` where `request.json` is the body returned by `GET /v1/defaults` (drop the `_meta` key). Unchanged, it is the free default project. ## Docs - OpenAPI: https://pvperf.com/openapi.json (every field carries its unit; fractions travel as 0–1). The answers are the schemas `RunAnswer` and `SimpleRunAnswer`, every series named in `SeriesBlock`. - Field pages: https://pvperf.com/v1/docs (manifest), https://pvperf.com/v1/docs/{key} (e.g. `array.tilt_deg`, `loss.iam`), and each page as markdown at https://pvperf.com/v1/docs/{key}.md - A run from a location and a size: https://pvperf.com/v1/docs/simple_run (every field, its default and the default's source) - Writing a scene: https://pvperf.com/v1/docs/scene_agents (the conventions, a recipe, converting a CAD, GIS or KML layout, every rejection with its fix); JSON Schemas at https://pvperf.com/v1/schema/scene.json and https://pvperf.com/v1/schema/run.json - East-west racking: https://pvperf.com/v1/docs/east_west (how the two faces run, the inputs, ridges in a scene, the example request, the result)