{"openapi":"3.1.0","info":{"title":"FirstPoint Solar Panel Database API","version":"1.0.0","summary":"Electrical specifications and datasheets for ~1,300 solar panel models.","description":"The panel database behind the FirstPoint Solar Panel String Sizer: STC and NOCT ratings, temperature coefficients, bifaciality and a downloadable datasheet PDF for every model that has one. Partners can also add panels: upload a datasheet, review the AI-extracted variants, then send an explicit save command. All requests need an API key.","contact":{"name":"FirstPoint Energy","url":"https://firstpoint.energy"},"termsOfService":"https://string-sizer.vercel.app/tou","x-disclaimer":"Data is provided \"as is\" without warranty of any kind and is not engineering advice. Verify every value against the manufacturer's current datasheet and applicable codes; FirstPoint Energy accepts no liability for any loss or damage arising from use of this data. See the terms of use."},"servers":[{"url":"https://string-sizer.vercel.app/api/v1","description":"Production"}],"security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"tags":[{"name":"Panels"},{"name":"Extractions","description":"Add panels from a datasheet PDF: extract, review, save"},{"name":"Meta"}],"paths":{"/panels":{"get":{"tags":["Panels"],"operationId":"listPanels","summary":"List or search panels","description":"Returns a page of panels. Every filter is optional and filters combine with AND. Page with `limit`/`offset`; `meta.nextOffset` is null on the last page.","parameters":[{"name":"q","in":"query","schema":{"type":"string"},"description":"Free-text search. Each whitespace-separated word must appear in the panel name (case-insensitive), e.g. `q=canadian 450`."},{"name":"ids","in":"query","schema":{"type":"string"},"description":"Comma-separated list of up to 100 panel ids, e.g. `ids=12,340,1105`."},{"name":"minWatts","in":"query","schema":{"type":"number","minimum":0},"description":"Minimum STC power (W), inclusive."},{"name":"maxWatts","in":"query","schema":{"type":"number","minimum":0},"description":"Maximum STC power (W), inclusive."},{"name":"bifacial","in":"query","schema":{"type":"boolean"},"description":"Only bifacial (true) or only monofacial (false) panels."},{"name":"verified","in":"query","schema":{"type":"boolean"},"description":"Only reviewed entries (true) or only unreviewed uploads (false)."},{"name":"hasDatasheet","in":"query","schema":{"type":"boolean"},"description":"Only panels with (true) or without (false) a downloadable datasheet."},{"name":"hasNoct","in":"query","schema":{"type":"boolean"},"description":"Only panels with (true) or without (false) NOCT ratings."},{"name":"createdAfter","in":"query","schema":{"type":"string","format":"date-time"},"description":"Only panels added after this ISO-8601 timestamp — use it for incremental syncs."},{"name":"sort","in":"query","schema":{"type":"string","enum":["name","watts","id","createdAt"],"default":"name"}},{"name":"order","in":"query","schema":{"type":"string","enum":["asc","desc"],"default":"asc"}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":500,"default":100}},{"name":"offset","in":"query","schema":{"type":"integer","minimum":0,"default":0}}],"responses":{"200":{"description":"A page of panels","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PanelList"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/panels/{id}":{"get":{"tags":["Panels"],"operationId":"getPanel","summary":"Get one panel","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"The panel","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/Panel"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/extractions/uploads":{"post":{"tags":["Extractions"],"operationId":"createExtractionUpload","summary":"Get a presigned URL for a large datasheet","description":"Returns a short-lived URL to PUT a datasheet PDF straight to storage, bypassing the platform request-body cap of about 4.5 MB that limits inline uploads to roughly 3 MB of PDF. PUT the bytes with Content-Type: application/pdf, then POST /extractions with {\"datasheetUrl\": \"...\"}. The URL accepts exactly one PDF at that path and expires in an hour.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"filename":{"type":"string","description":"Original filename, used to label the stored datasheet"}}}}}},"responses":{"201":{"description":"Upload URL issued","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"object","required":["uploadUrl","datasheetUrl","expiresAt","maxBytes"],"properties":{"uploadUrl":{"type":"string","format":"uri","description":"PUT the PDF here with Content-Type: application/pdf"},"datasheetUrl":{"type":"string","format":"uri","description":"Pass this to POST /extractions once the PUT succeeds"},"expiresAt":{"type":"string","format":"date-time"},"maxBytes":{"type":"integer","description":"Largest PDF the URL will accept"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"503":{"description":"Datasheet uploads are not configured on this server"}}}},"/extractions":{"post":{"tags":["Extractions"],"operationId":"createExtraction","summary":"Upload a datasheet PDF for AI extraction","description":"Runs the datasheet through the FirstPoint AI extractor and returns one draft per panel variant (power class) found. Nothing is written to the panel database until you call save. Extractions expire after 24 hours. Maximum PDF size 25 MB. Bodies sent inline (raw, multipart or base64) must stay under the platform request-body cap of about 4.5 MB; for larger datasheets get an upload URL from POST /extractions/uploads first and send its datasheetUrl.","requestBody":{"required":true,"content":{"application/pdf":{"schema":{"type":"string","format":"binary"}},"application/octet-stream":{"schema":{"type":"string","format":"binary"}},"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","format":"binary","description":"The datasheet PDF (any field name; the first file part is used)"}}}},"application/json":{"schema":{"type":"object","description":"Either an inline base64 PDF, or a datasheetUrl from POST /extractions/uploads. Inline bodies are limited to about 3 MB by the platform request-body cap; use datasheetUrl for anything larger.","oneOf":[{"required":["pdf"]},{"required":["datasheetUrl"]}],"properties":{"pdf":{"type":"string","contentEncoding":"base64","description":"Base64-encoded PDF (a data: URL prefix is tolerated). Keep the whole request under ~4.5 MB; larger datasheets must go through POST /extractions/uploads."},"datasheetUrl":{"type":"string","format":"uri","description":"The datasheetUrl returned by POST /extractions/uploads, after the PDF has been PUT to its uploadUrl. Any other URL is rejected."},"filename":{"type":"string","description":"Original filename, used to label the archived datasheet"}}}}}},"parameters":[{"name":"X-Filename","in":"header","schema":{"type":"string"},"description":"Original filename for raw uploads; the filename query parameter works too."}],"responses":{"201":{"description":"Drafts extracted (not saved)","headers":{"Location":{"schema":{"type":"string"},"description":"URL of the extraction"}},"content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/Extraction"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"413":{"description":"PDF larger than 25 MB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"Unsupported Content-Type","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"The document could not be processed or contains no panel specifications","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"The AI extraction failed; retry later","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/extractions/{id}":{"get":{"tags":["Extractions"],"operationId":"getExtraction","summary":"Retrieve an extraction","description":"Only the API key that created the extraction can read it.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","pattern":"^ext_"}}],"responses":{"200":{"description":"The extraction","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/Extraction"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/extractions/{id}/save":{"post":{"tags":["Extractions"],"operationId":"saveExtraction","summary":"Save reviewed drafts to the panel database","description":"The save command. With no body (or no 'panels'), every draft is saved as extracted. Otherwise list the drafts to save by index, optionally overriding any reviewable field. Saved panels start with verified: false and link to the archived datasheet. A draft whose name already exists in the database is skipped, not overwritten. Each extraction can be saved once.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","pattern":"^ext_"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SaveRequest"}}}},"responses":{"200":{"description":"Result of the save","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/SaveResult"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"This extraction was already saved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"This extraction expired (24 h); upload the datasheet again","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/openapi.json":{"get":{"tags":["Meta"],"operationId":"getOpenApi","summary":"This document","security":[],"responses":{"200":{"description":"OpenAPI 3.1 document","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/":{"get":{"tags":["Meta"],"operationId":"getIndex","summary":"API index","security":[],"responses":{"200":{"description":"Endpoint list and documentation link","content":{"application/json":{"schema":{"type":"object"}}}}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"`Authorization: Bearer <key>`"},"apiKeyHeader":{"type":"apiKey","in":"header","name":"X-API-Key"}},"responses":{"BadRequest":{"description":"A query parameter is invalid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Unauthorized":{"description":"Missing, invalid or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"No such panel or route","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"schemas":{"Panel":{"type":"object","required":["id","name","verified","bifacial","stc","noct","bifacialWatts","ptcWatts","tempCoeff","datasheetUrl","createdAt"],"properties":{"id":{"type":"integer","description":"Stable panel id"},"name":{"type":"string","description":"Brand and model, e.g. \"Canadian Solar CS6.1-54TM-450H\"","examples":["Aptos Solar DNA-108-MF10-390W"]},"verified":{"type":"boolean","description":"true once a FirstPoint reviewer has checked the entry; false for fresh datasheet uploads"},"bifacial":{"type":"boolean"},"stc":{"type":"object","description":"Ratings at Standard Test Conditions (1000 W/m², 25 °C cell, AM1.5)","required":["watts","voc","vmp","imp","isc"],"properties":{"watts":{"type":"number","description":"Pmax (W)"},"voc":{"type":"number","description":"Open-circuit voltage (V)"},"vmp":{"type":"number","description":"Voltage at max power (V)"},"imp":{"type":"number","description":"Current at max power (A)"},"isc":{"type":"number","description":"Short-circuit current (A)"}}},"noct":{"type":["object","null"],"description":"Ratings at Nominal Operating Cell Temperature (800 W/m², 20 °C ambient, 1 m/s wind), or null if the datasheet lists none","required":["watts","imp","isc"],"properties":{"watts":{"type":"number","description":"Pmax at NOCT (W)"},"imp":{"type":["number","null"],"description":"Imp at NOCT (A), null if not listed"},"isc":{"type":["number","null"],"description":"Isc at NOCT (A), null if not listed"}}},"bifacialWatts":{"type":["number","null"],"description":"Rear-side-gain power rating from the datasheet (W), null if not listed"},"ptcWatts":{"type":["number","null"],"description":"PVUSA Test Conditions rating (W), null if not listed"},"tempCoeff":{"type":"object","description":"Temperature coefficients in %/°C (negative for Voc and Pmp)","required":["voc","pmp","isc"],"properties":{"voc":{"type":"number","description":"Voc coefficient (%/°C)","examples":[-0.27]},"pmp":{"type":"number","description":"Pmax coefficient (%/°C)","examples":[-0.34]},"isc":{"type":"number","description":"Isc coefficient (%/°C)","examples":[0.05]}}},"datasheetUrl":{"type":["string","null"],"format":"uri","description":"Direct, public download URL of the manufacturer datasheet PDF, or null if none is on file"},"createdAt":{"type":"string","format":"date-time","description":"When the entry was added to the database"}}},"PanelList":{"type":"object","required":["data","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Panel"}},"meta":{"type":"object","required":["total","limit","offset","nextOffset"],"properties":{"total":{"type":"integer","description":"Panels matching the filters"},"limit":{"type":"integer"},"offset":{"type":"integer"},"nextOffset":{"type":["integer","null"],"description":"Pass as `offset` to fetch the next page; null on the last page"}}}}},"PanelDraft":{"type":"object","description":"An AI-extracted panel variant awaiting review: the Panel fields minus id/verified/datasheetUrl/createdAt, plus index, brand and website.","required":["index","name","brand","website","bifacial","stc","noct","bifacialWatts","ptcWatts","tempCoeff"],"properties":{"index":{"type":"integer","description":"Position within the extraction; reference it in the save command"},"name":{"type":"string","description":"Proposed database name ('Brand Model')"},"brand":{"type":["string","null"]},"website":{"type":["string","null"],"description":"Manufacturer domain as printed on the datasheet"},"bifacial":{"type":"boolean"},"stc":{"$ref":"#/components/schemas/Panel/properties/stc"},"noct":{"$ref":"#/components/schemas/Panel/properties/noct"},"bifacialWatts":{"type":["number","null"],"description":"Always null from extraction; set it in the save command if the datasheet lists a rear-gain rating"},"ptcWatts":{"type":["number","null"],"description":"PTC rating (W) if printed, else null"},"tempCoeff":{"$ref":"#/components/schemas/Panel/properties/tempCoeff"}}},"Extraction":{"type":"object","required":["id","status","filename","datasheetUrl","panels","savedPanelIds","createdAt","expiresAt","savedAt"],"properties":{"id":{"type":"string","examples":["ext_Qm9vZ2llV29vZ2ll"]},"status":{"type":"string","enum":["pending","saved","expired"]},"filename":{"type":"string"},"datasheetUrl":{"type":["string","null"],"format":"uri","description":"Archived copy of the uploaded PDF; attached to every panel saved from it"},"panels":{"type":"array","items":{"$ref":"#/components/schemas/PanelDraft"}},"savedPanelIds":{"type":"array","items":{"type":"integer"},"description":"Ids of the panels created by save (empty until then)"},"createdAt":{"type":"string","format":"date-time"},"expiresAt":{"type":"string","format":"date-time"},"savedAt":{"type":["string","null"],"format":"date-time"}}},"SaveRequest":{"type":"object","properties":{"panels":{"type":"array","minItems":1,"description":"Drafts to save, by index, each with optional reviewed overrides. Omit the whole body to save every draft unchanged.","items":{"type":"object","required":["index"],"properties":{"index":{"type":"integer"},"name":{"type":"string","maxLength":120},"bifacial":{"type":"boolean"},"stc":{"type":"object","description":"Any subset of watts, voc, vmp, imp, isc","additionalProperties":{"type":"number"}},"noct":{"type":["object","null"],"description":"Any subset of watts, imp, isc, or null to clear","additionalProperties":{"type":["number","null"]}},"bifacialWatts":{"type":["number","null"]},"ptcWatts":{"type":["number","null"]},"tempCoeff":{"type":"object","description":"Any subset of voc, pmp, isc (%/°C)","additionalProperties":{"type":"number"}}}}}}},"SaveResult":{"type":"object","required":["extraction","saved","skipped"],"properties":{"extraction":{"$ref":"#/components/schemas/Extraction"},"saved":{"type":"array","items":{"$ref":"#/components/schemas/Panel"},"description":"Panels created (verified: false)"},"skipped":{"type":"array","items":{"type":"object","required":["index","name","reason","message"],"properties":{"index":{"type":"integer"},"name":{"type":"string"},"reason":{"type":"string","enum":["duplicate"]},"message":{"type":"string"}}}}}},"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","enum":["bad_request","unauthorized","not_found","method_not_allowed","conflict","gone","payload_too_large","unsupported_media_type","unprocessable","rate_limited","extraction_failed","not_configured","internal"]},"message":{"type":"string"}}}}}}}}