TypeScript example
This script runs a small house and a 5 MWp tracker field through pvperf's REST API.
The script needs Node.js 22.18 or later and no package. Set the environment variable PVPERF_API_KEY to your key from the workbench's Account page, then run node pvperf_first_run.ts.
// A first run of a small house and of a tracker field through pvperf's REST API.
//
// Needs Node.js 22.18 or later, which runs a TypeScript file as it is.
// Set PVPERF_API_KEY to your key from the workbench's Account page, then run:
//
// node pvperf_first_run.ts
const BASE_URL = process.env.PVPERF_BASE_URL ?? "https://pvperf.com";
const API_KEY = process.env.PVPERF_API_KEY ?? "";
interface MonthRow {
month: number;
e_grid_kwh: number;
}
interface LossRow {
label: string;
kind: string;
value: number;
unit: string;
}
interface Answer {
summary: {
p_nom_dc_kwp: number;
e_grid_kwh: number;
specific_yield_kwh_per_kwp: number;
performance_ratio: number;
monthly: MonthRow[];
};
loss_diagram: LossRow[];
billing: { units: number; price_usd: number };
request: { array: Record<string, unknown> } & Record<string, unknown>;
series?: Record<string, number[]>;
}
/** POST a JSON body to pvperf and return the answer. A refused request throws with pvperf's
* reason. A refused run is not charged. */
async function post(path: string, body: object): Promise<Answer> {
const r = await fetch(BASE_URL + path, {
method: "POST",
headers: { Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const answer = await r.json();
if (r.status >= 400) {
const problem = answer as { code: string; detail: string };
throw new Error(`${r.status} ${problem.code}: ${problem.detail}`);
}
return answer as Answer;
}
const n = (x: number, digits = 0) => x.toLocaleString("en-US", { maximumFractionDigits: digits, minimumFractionDigits: digits });
function show(name: string, answer: Answer): void {
const s = answer.summary;
console.log(`${name}: ${n(s.p_nom_dc_kwp, 1)} kWp, ${n(s.e_grid_kwh)} kWh to the grid a year`);
console.log(` specific yield ${n(s.specific_yield_kwh_per_kwp)} kWh/kWp`);
console.log(` performance ratio ${n(s.performance_ratio * 100, 1)}%`);
const losses = answer.loss_diagram.filter((r) => r.kind === "loss" && r.unit === "kWh/kWp");
for (const r of losses.sort((a, b) => a.value - b.value).slice(0, 3)) {
console.log(` ${r.label}: ${n(r.value, 1)} kWh/kWp`);
}
console.log(` charged ${answer.billing.units} unit(s), USD ${answer.billing.price_usd.toFixed(3)}`);
}
async function main(): Promise<void> {
if (!API_KEY) throw new Error("Set PVPERF_API_KEY to your pvperf API key.");
// 1. A house: 16 modules of 400 W flush on a south-facing roof at 25° in Springfield, Illinois.
// The answer also carries the hourly power to the grid, p_grid_w, in W.
const house = await post("/v1/run/simple", {
latitude: 39.8,
longitude: -89.65,
racking: "rooftop",
modules: 16,
module_power_w: 400,
tilt_deg: 25,
azimuth_deg: 180,
include_series: ["p_grid_w"],
});
show("House", house);
for (const m of house.summary.monthly) {
console.log(` month ${String(m.month).padStart(2)}: ${n(m.e_grid_kwh).padStart(7)} kWh`);
}
const hourlyW = house.series?.p_grid_w ?? [];
console.log(` ${hourlyW.length} hourly values, highest ${n(Math.max(...hourlyW) / 1000, 2)} kW`);
// 2. A tracker field: 5 MWp of single-axis trackers near Phoenix, Arizona.
const field = await post("/v1/run/simple", {
latitude: 33.45,
longitude: -112.07,
racking: "single_axis",
system_capacity_kw: 5000,
});
show("Tracker field, ground cover ratio 0.40", field);
// 3. The same field with the rows further apart: change the full request the simple run
// returned and send it to POST /v1/run.
const request = field.request;
request.array.gcr = 0.33;
const wider = await post("/v1/run", request);
show("Tracker field, ground cover ratio 0.33", wider);
}
main().catch((err) => {
console.error(err instanceof Error ? err.message : err);
process.exitCode = 1;
});
What it prints
- For the house: the DC nameplate in kWp, the energy to the grid in kWh, the specific yield in kWh/kWp, the performance ratio, the three largest energy losses in kWh/kWp, the units charged and the energy to the grid of each month in kWh.
- The count of hourly values, 8,760, and the highest hourly power to the grid in kW.
- For the tracker field: the same summary at a ground cover ratio of 0.40, then at 0.33.
The script makes three runs. Each answer's billing block gives the units and the price of its run.
The requests
POST /v1/run/simpletakes a location and a system size. Its answer carriesrequest, the full request it ran, anddefaults_applied, each value it assumed with its source. The simple run guide lists every field.POST /v1/runtakes a full request. The script sends the simple run'srequestwith a newarray.gcr. The reference documents each field."include_series": ["p_grid_w"]adds the named hourly series to the answer.trueadds every series.
A refused request answers with a problem document. Its code and detail say why, and field names the field to fix. A run that fails is not charged.
A run predicted to take more than 40 s answers 202 with status running. GET /v1/runs/{run_id} returns it when it is done. Each run of this script answers in under 15 s.
The OpenAPI document gives the types of every field and answer. npx openapi-typescript https://pvperf.com/openapi.json -o pvperf.d.ts writes them as TypeScript types. The Python example makes the same runs.