Request access
Keys are issued by hand to partners and integrators. Tell us who you are and what you are building, and we will email you back with a key.
A REST API over the panel database used by the FirstPoint Solar Panel String Sizer: roughly 1,300 models with STC and NOCT ratings, temperature coefficients, bifaciality, and a direct download link to the manufacturer datasheet wherever one is on file. Partners can also add panels by uploading a datasheet: our AI extracts every variant, you review the result, and nothing is saved until you say so.
All responses are JSON (UTF-8). Successful responses put the payload under data;
list responses add a meta object for paging. The API is versioned in the path —
breaking changes will ship as /api/v2, and v1 will keep working.
New fields may be added to v1 responses at any time, so ignore keys you do not recognise.
Authentication
Every panel request needs an API key. Contact FirstPoint Energy to request one. Send it in either of these headers:
Authorization: Bearer fpk_…
# or
X-API-Key: fpk_…
Requests without a valid key get 401. Keep the key server-side: the API allows
cross-origin browser requests (CORS *), but a key embedded in public JavaScript is
visible to anyone and will be revoked if abused.
Endpoints
/panelsAPI keyReturns a page of panels. All parameters are optional and combine with AND.
| Parameter | Type | Description |
|---|---|---|
q | string | Free-text search. Every whitespace-separated word must appear in the panel name, case-insensitively — q=canadian 450 finds every Canadian Solar 450 W model. Up to 10 words. |
ids | string | Comma-separated panel ids, up to 100 — ids=12,340,1105. |
minWatts | number | Minimum STC power in watts, inclusive. |
maxWatts | number | Maximum STC power in watts, inclusive. |
bifacial | boolean | true for bifacial panels only, false for monofacial only. |
verified | boolean | true for reviewed entries only; false for entries added from datasheet uploads that have not been reviewed yet. |
hasDatasheet | boolean | Whether a downloadable datasheet PDF is on file. |
hasNoct | boolean | Whether NOCT ratings are recorded. |
createdAfter | date-time | ISO-8601 timestamp; only panels added after it. Use with sort=createdAt for incremental syncs. |
sort | enum | name (default), watts, id, createdAt. Ties break on id. |
order | enum | asc (default) or desc. |
limit | integer | Page size, 1–500. Default 100. |
offset | integer | Rows to skip. Default 0. |
The response carries the page under data and paging info under meta.
Keep requesting with offset=meta.nextOffset until it is null.
{
"data": [ { "id": 225, "name": "Canadian Solar CS6.1-54TM-450H", … } ],
"meta": { "total": 8, "limit": 100, "offset": 0, "nextOffset": null }
}
/panels/{id}API keyReturns a single panel by its numeric id, wrapped in data. Unknown ids return 404.
/openapi.jsonno keyThe machine-readable OpenAPI 3.1 description of this API —
import it into Postman, Insomnia, or a client generator. GET /api/v1 (no key) returns a short
index with links to the endpoints and this page.
The panel object
| Field | Type | Description |
|---|---|---|
id | integer | Stable identifier. Never reused. |
name | string | Brand and model as printed on the datasheet, e.g. "Aptos Solar DNA-108-MF10-390W" (id 32). |
verified | boolean | true once a FirstPoint reviewer has checked the entry. New entries created from datasheet uploads start as false. |
bifacial | boolean | Bifacial module. |
stc | object | Ratings at Standard Test Conditions (1000 W/m², 25 °C cell, AM1.5): watts (Pmax, W), voc (V), vmp (V), imp (A), isc (A). Always present. |
noct | object | null | Ratings at Nominal Operating Cell Temperature (800 W/m², 20 °C ambient, 1 m/s wind): watts, imp, isc. null when the datasheet lists none; imp/isc may individually be null. Voltages at NOCT are not stored — string-voltage limits should always use STC Voc and Vmp with the temperature coefficients. |
bifacialWatts | number | null | Rear-side-gain power rating quoted by the manufacturer, when listed. |
ptcWatts | number | null | PVUSA Test Conditions rating, when listed. |
tempCoeff | object | Temperature coefficients in %/°C: voc and pmp (negative, e.g. -0.27), isc (positive, e.g. 0.05). |
datasheetUrl | string | null | Direct, public URL of the manufacturer datasheet PDF, or null. See Datasheets. |
createdAt | date-time | When the entry was added to the database (ISO-8601, UTC). |
{
"id": 16,
"name": "Alexus Solar ALEX-400-B-54-S",
"verified": true,
"bifacial": false,
"stc": { "watts": 400, "voc": 37.21, "vmp": 31.18, "imp": 12.83, "isc": 13.67 },
"noct": { "watts": 302.3, "imp": 10.58, "isc": 11.04 },
"bifacialWatts": null,
"ptcWatts": 370,
"tempCoeff": { "voc": -0.26, "pmp": -0.34, "isc": 0.05 },
"datasheetUrl": "https://1jlpnihnya4zwkzy.public.blob.vercel-storage.com/datasheets/16.pdf",
"createdAt": "2026-08-18T22:20:20.403Z"
}
Datasheets
datasheetUrl is a plain HTTPS link to the PDF on FirstPoint's file storage. It needs
no API key, so you can hand it straight to end users or fetch it server-side. Most files are the
manufacturer's original datasheet; a few are archived copies where the manufacturer's link has since
gone dead. Filter with hasDatasheet=true when you only want panels that have one.
Adding panels
Adding a panel is a three-step flow so that nothing lands in the shared database without a human
(or your own validation code) looking at it first. Every saved panel is linked to the archived
datasheet and starts as verified: false until a FirstPoint reviewer checks it.
/extractionsAPI key1 · Upload the datasheet. Send the PDF in whichever way is convenient:
| Content-Type | Body |
|---|---|
application/pdf | The raw PDF bytes. Put the original filename in an X-Filename header (or ?filename=). |
multipart/form-data | A form upload with one file field (any name) — what curl -F and browsers send. |
application/json | { "pdf": "<base64>", "filename": "…" } |
application/json | { "datasheetUrl": "…" } — a datasheet already uploaded via the two-step route. Required above ~3 MB. |
Datasheets over ~3 MB. The first three forms carry the PDF inside the request, and our platform rejects request bodies larger than about 4.5 MB before they reach the API — which, after base64's one-third overhead, caps an inline upload at roughly 3 MB of PDF. About one panel datasheet in eight is bigger than that. For those, get a presigned URL first and PUT the bytes straight to storage:
/extractions/uploadsAPI key# 1a. ask for somewhere to put it (filename is optional)
curl -X POST -H "Authorization: Bearer $FP_API_KEY" -H "Content-Type: application/json" \
-d '{"filename":"JAM60-500LB.pdf"}' \
https://string-sizer.vercel.app/api/v1/extractions/uploads
# → {"data":{"uploadUrl":"https://…","datasheetUrl":"https://…","expiresAt":"…","maxBytes":26214400}}
# 1b. PUT the PDF to uploadUrl — no auth header, the URL is the credential
curl -X PUT -H "Content-Type: application/pdf" --data-binary @JAM60-500LB.pdf "$UPLOAD_URL"
# 1c. then extract from it
curl -X POST -H "Authorization: Bearer $FP_API_KEY" -H "Content-Type: application/json" \
-d "{\"datasheetUrl\":\"$DATASHEET_URL\"}" \
https://string-sizer.vercel.app/api/v1/extractions
The URL accepts exactly one PDF at one path, caps it at 25 MB, and expires in an hour. Anything that is not a PDF is rejected and deleted. From step 1c on, the flow is identical to an inline upload.
The PDF (max 25 MB) goes through our AI extractor, which reads every power class in the datasheet's electrical tables. Expect the call to take 20–60 seconds. The response is an extraction: an id, the archived datasheet URL, and one draft per variant. Nothing has been saved yet.
{
"data": {
"id": "ext_Qm9vZ2llV29vZ2ll",
"status": "pending",
"filename": "CS6.1-54TM datasheet.pdf",
"datasheetUrl": "https://….public.blob.vercel-storage.com/datasheets/uploads/…pdf",
"panels": [
{ "index": 0, "name": "Canadian Solar CS6.1-54TM-450H", "brand": "Canadian Solar",
"website": "csisolar.com", "bifacial": false,
"stc": { "watts": 450, "voc": 41.5, "vmp": 34.9, "imp": 12.9, "isc": 13.6 },
"noct": { "watts": 338, "imp": 9.75, "isc": 10.9 },
"bifacialWatts": null, "ptcWatts": null,
"tempCoeff": { "voc": -0.26, "pmp": -0.34, "isc": 0.05 } },
{ "index": 1, "name": "Canadian Solar CS6.1-54TM-455H", … }
],
"savedPanelIds": [],
"createdAt": "2026-09-04T22:41:07.512Z",
"expiresAt": "2026-09-05T22:41:07.512Z",
"savedAt": null
}
}
/extractions/{id}API key2 · Review. Check the drafts against the datasheet — the AI is accurate on clean tables but can misread scanned or unusual layouts. You can fetch the extraction again at any time within 24 hours (only with the key that created it), which lets a separate reviewer or job do the checking. Anything you want to correct goes into the save command; the extraction itself is immutable.
/extractions/{id}/saveAPI key3 · Save. The explicit command that writes to the database. Send no body to save
every draft exactly as extracted, or choose drafts by index and override any reviewable
field: name, bifacial, stc, noct (an object or
null), bifacialWatts, ptcWatts, tempCoeff. Objects
merge field by field, so { "stc": { "voc": 41.6 } } changes only Voc.
{
"panels": [
{ "index": 0 },
{ "index": 1, "name": "Canadian Solar CS6.1-54TM-455H", "stc": { "voc": 41.9 }, "ptcWatts": 412 }
]
}
The response lists the panels that were created (with their new ids, ready to use in
GET /panels/{id}) and any that were skipped because a panel with the same name already
exists — existing entries are never overwritten. Each extraction can be saved once; a second save
returns 409, and an extraction older than 24 hours returns 410.
{
"data": {
"extraction": { "id": "ext_Qm9vZ2llV29vZ2ll", "status": "saved", "savedPanelIds": [1312], … },
"saved": [ { "id": 1312, "name": "Canadian Solar CS6.1-54TM-455H", "verified": false, … } ],
"skipped": [ { "index": 0, "name": "Canadian Solar CS6.1-54TM-450H", "reason": "duplicate",
"message": "A panel with this name already exists." } ]
}
}
The whole flow with curl
# 1. upload (datasheets over ~3 MB: see the two-step route above)
curl -X POST -H "Authorization: Bearer $FP_API_KEY" \
-H "Content-Type: application/pdf" -H "X-Filename: CS6.1-54TM.pdf" \
--data-binary @CS6.1-54TM.pdf \
https://string-sizer.vercel.app/api/v1/extractions
# 2. review the JSON above, then
# 3. save everything as extracted …
curl -X POST -H "Authorization: Bearer $FP_API_KEY" \
https://string-sizer.vercel.app/api/v1/extractions/ext_Qm9vZ2llV29vZ2ll/save
# … or only the variants you checked, with corrections
curl -X POST -H "Authorization: Bearer $FP_API_KEY" -H "Content-Type: application/json" \
-d '{"panels":[{"index":1,"stc":{"voc":41.9}}]}' \
https://string-sizer.vercel.app/api/v1/extractions/ext_Qm9vZ2llV29vZ2ll/save
MCP server
The same database, extraction flow and the String Sizer's own sizing engine are also available as a Model Context Protocol server, so AI assistants and agents can use them directly. It speaks Streamable HTTP, is stateless, and authenticates with the same API key sent as a bearer token.
| Tool | What it does |
|---|---|
search_panels | Search and filter the database (same filters as GET /panels). |
get_panel | Full specifications and datasheet link for one panel. |
list_inverters | The inverters, energy controllers and charge controllers the sizer knows, with their MPPT limits. |
size_string | Run the string-sizing calculation: inverter + panel + mounting + site temperatures → panels per string, strings per MPPT, proposed array and the per-MPPT electrical report with warnings. |
extract_datasheet | Fetch a datasheet PDF from a URL (or take it as base64) and return AI-extracted drafts. Nothing is saved. |
get_extraction | Re-read a pending extraction. |
save_extraction | The save command. Marked as a write, so well-behaved clients ask the user first. |
Claude Code
claude mcp add --transport http firstpoint-panels https://string-sizer.vercel.app/api/mcp \
--header "Authorization: Bearer $FP_API_KEY"
Cursor, Windsurf and other JSON-configured clients
{
"mcpServers": {
"firstpoint-panels": {
"url": "https://string-sizer.vercel.app/api/mcp",
"headers": { "Authorization": "Bearer fpk_…" }
}
}
}
Claude Messages API (MCP connector)
{
"model": "claude-opus-5",
"max_tokens": 16000,
"mcp_servers": [{
"type": "url",
"url": "https://string-sizer.vercel.app/api/mcp",
"name": "firstpoint-panels",
"authorization_token": "fpk_…"
}],
"tools": [{ "type": "mcp_toolset", "mcp_server_name": "firstpoint-panels" }],
"messages": [{ "role": "user", "content": "How many Canadian Solar 450 W panels per string on a FlexBOSS21 in Calgary?" }]
}
Send the header anthropic-beta: mcp-client-2025-11-20 with that request. Clients that only
support OAuth-based remote servers (for example the custom-connector directory in the Claude apps) are not
supported yet; bearer-token clients are.
Extraction through MCP takes the same 20–60 seconds as the REST call, and the same terms apply: the model is reading transcribed data, not a verified design.
Errors
Errors use standard HTTP status codes and a JSON body with a stable code and a human-readable message.
{ "error": { "code": "bad_request", "message": "limit must be at most 500" } }
| Status | Code | When |
|---|---|---|
| 400 | bad_request | A query parameter is malformed or out of range. Unknown parameters are ignored. |
| 401 | unauthorized | Missing, invalid, or revoked API key. |
| 404 | not_found | No panel with that id, or no such route. |
| 405 | method_not_allowed | Wrong HTTP method for that route; the Allow header lists the right ones. |
| 409 | conflict | The extraction was already saved. |
| 410 | gone | The extraction expired (24 hours); upload the datasheet again. |
| 413 | payload_too_large | PDF larger than 25 MB. |
| 415 | unsupported_media_type | Upload sent with a Content-Type the API does not accept. |
| 422 | unprocessable | The PDF could not be processed, or no panel specifications were found in it. |
| 429 | rate_limited | Too many access requests from one address. |
| 502 | extraction_failed | The AI extraction failed; retry in a minute. |
| 503 | not_configured | The service is temporarily unavailable; retry later. |
| 500 | internal | Unexpected failure; retry with backoff. |
Examples
Search with curl
curl -H "Authorization: Bearer $FP_API_KEY" \
"https://string-sizer.vercel.app/api/v1/panels?q=rec%20pure&minWatts=400&hasDatasheet=true"
Fetch one panel
curl -H "X-API-Key: $FP_API_KEY" https://string-sizer.vercel.app/api/v1/panels/16
Download every panel (JavaScript)
const BASE = 'https://string-sizer.vercel.app/api/v1';
const headers = { authorization: `Bearer ${process.env.FP_API_KEY}` };
async function allPanels() {
const panels = [];
for (let offset = 0; offset !== null;) {
const res = await fetch(`${BASE}/panels?limit=500&offset=${offset}`, { headers });
if (!res.ok) throw new Error(`${res.status}: ${(await res.json()).error.message}`);
const { data, meta } = await res.json();
panels.push(...data);
offset = meta.nextOffset;
}
return panels;
}
Incremental sync
Remember the newest createdAt you have seen and ask only for what came after it:
GET /panels?createdAfter=2026-08-18T22:20:24.902Z&sort=createdAt&order=asc
Terms of use
Use of the API is subject to the FirstPoint Energy Terms of Use. In short: the data is provided “as is” with no warranty, it is not engineering advice, you must verify every value against the manufacturer’s current datasheet and applicable codes, and FirstPoint Energy accepts no liability for any loss arising from its use. By requesting a key you accept those terms.
Practical notes
- Data quality. Entries with
verified: falsewere extracted automatically from an uploaded PDF and have not been reviewed — check them against the linked datasheet before relying on them for design work. Verified entries can still be wrong; verify those too. - Caching. Panel responses are marked
Cache-Control: private, max-age=60. The full dataset changes rarely; pulling it once a day is plenty. - Fair use. No hard rate limit is enforced today. Please keep sustained traffic under a few requests per second and use paging rather than repeated full downloads; abusive keys are revoked.
- Uploads. Only upload datasheets you are entitled to share; archived copies are
served publicly as
datasheetUrl. Panels you save are visible to every API consumer and to String Sizer users, flagged unverified until reviewed. - Keys. Keys are personal to the partner they were issued to, may not be shared or embedded in public code, and may be revoked at any time.
- Attribution. When you show this data to your users, please credit FirstPoint Energy and pass the disclaimer on to them.
