Pipe Solar API
Version v1. Read and send your company's leads and projects from your own systems and AI agents.
Getting started
An owner or admin makes a key in Pipe under Settings → API Keys. The key shows once; keep it in your system's secret store. It reaches every endpoint below, for your own company only. Call from a server, never from a web page: a key in a page's code is a key anyone can read.
GET https://staging-api.pipe.solar/v1/me Authorization: Bearer psk_<40 characters>
Every answer is JSON. An error answers its status with one shape, and the message says what to change.
{
"error": { "code": "unauthorized", "message": "Send the API key as Authorization: Bearer <key>." }
}The same list as an OpenAPI file, for your tools and agents: openapi.json
Errors, retries and limits
| Code | Status | What it means |
|---|---|---|
| unauthorized | 401 | No key, or a key that is wrong, revoked, or whose company is not active. Check the key; do not retry. |
| forbidden | 403 | The key is right but the endpoint is not for its company, such as a dealer reading the installer's catalogue. |
| not_found | 404 | No such address, or no record with this id is yours. Another company's record answers the same as a missing one. |
| invalid | 400 | The request is not valid; the message says what to change. Retrying the same request fails the same way. |
| rate_limited | 429 | The key made more than 100 calls in a minute. Wait the seconds `Retry-After` names, then retry. |
| internal | 500 | Pipe's side failed. Retry after a short wait; if it repeats, send Pipe the answer's X-Request-Id. |
Safe to retry
- Every GET is safe to retry.
- Moving a stage to the stage the project is already in changes nothing, and answers
changed: false. - Setting the install date or NTP to the value it already holds changes nothing.
- Adding a file whose address this folder already holds adds nothing, and answers
added: false. - Creating a lead with an
externalIdyou sent before answers that lead, with 200, instead of making a second one.
Limits
- A key makes at most 100 calls a minute, every endpoint counted together; past that the answer is 429 with
Retry-After. - A list page holds at most 100 rows.
- A call adds at most 10 files, each 100 MB or less.
- Every answer carries
X-Request-Id, the id of the call's own log line at Pipe.
Check a key
/v1/meAnswers the key's name and the company it belongs to. Use it to prove a key works before you build on it.
| Status | When |
|---|---|
| 200 | The key works. |
| 401 | The key is missing, wrong or revoked. |
GET https://staging-api.pipe.solar/v1/me Authorization: Bearer psk_<40 characters>
{
"key": { "name": "Make scenario", "prefix": "psk_3f9a1c2b" },
"company": { "name": "Inty Power" }
}List projects
/v1/projectsYour company's projects, solar and HI, newest change first, a page at a time. Keep the newest `updatedAt` you read and send it as `updatedSince` next time to read only what changed. An installer's key lists every project it builds, its dealers' included.
| Field | Type | Required | What it is |
|---|---|---|---|
| limit | number | No | Rows in a page, 1 to 100. 50 when absent. |
| cursor | string | No | The `next` of the page before, URL-encoded, with the same other fields. Absent for the first page. |
| updatedSince | string | No | Only rows changed after this time, ISO 8601 (2026-10-01T00:00:00Z). |
| productLine | string | No | `solar` or `hi`. Both when absent. |
| Status | When |
|---|---|
| 200 | `data` holds the page's projects, the core of each; `next` is the cursor for the next page, or null at the end. |
| 400 | The request is not valid; the message says what to change. |
| 401 | The key is missing, wrong or revoked. |
GET https://staging-api.pipe.solar/v1/projects Authorization: Bearer psk_<40 characters>
{
"data": [{ "id": "k97f…", "reference": "D4YR1VSK7K", "productLine": "solar", "stage": { "name": "Permitting", "enteredAt": "…" }, "updatedAt": "…" }],
"next": "eyJ…"
}Read a project
/v1/projects/{id}One project in full: the customer, the people, the contract, the financing, the milestones and the files. A solar project carries `system` and an HI project `products`; `productLine` says which, and the other is null. `{id}` is Pipe's id, or the reference your system holds from the old app (its 10-character UID). A project also answers to its lead's id.
| Status | When |
|---|---|
| 200 | The project. |
| 404 | No record with this id is yours. |
| 401 | The key is missing, wrong or revoked. |
GET https://staging-api.pipe.solar/v1/projects/{id}
Authorization: Bearer psk_<40 characters>{
"id": "k97f…",
"reference": "D4YR1VSK7K",
"leadId": "j57a…",
"productLine": "solar",
"stage": { "name": "Permitting", "enteredAt": "2026-10-02T14:05:00.000Z" },
"soldAt": "2026-09-20T18:31:00.000Z",
"updatedAt": "2026-10-02T14:05:00.000Z",
"ntpGranted": true,
"customer": { "firstName": "Ana", "lastName": "Rivera", "email": "ana@example.com", "phone": "602-555-0100" },
"coBorrower": null,
"address": { "line1": "812 Main St", "line2": null, "city": "Plano", "state": "TX", "postalCode": "75023", "country": "US" },
"notes": { "customer": null, "project": "Gate code 1234" },
"company": { "name": "Inty Power" },
"salesRep": { "name": "Jordan Lee", "email": "jordan@example.com", "phone": null },
"setter": null, "projectManager": null, "siteSurveyor": null, "subcontractor": null,
"contract": { "signedAt": "2026-09-20T18:31:00.000Z", "price": 28450.5, "files": ["https://…"] },
"financing": { "type": "loan", "lender": "GoodLeap", "amount": 28450.5, "aprPercent": 3.99, "termMonths": 300, "monthlyPayment": 142.18, "dealerFeePercent": 18 },
"system": {
"sizeKw": 8.2, "annualProductionKwh": 11480,
"panels": { "count": 20, "manufacturer": "REC", "model": "Alpha Pure 410" },
"inverters": { "count": 20, "manufacturer": "Enphase", "model": "IQ8M", "name": null, "microinverter": true },
"batteries": null,
"pricePerWatt": { "base": 3.1, "final": 3.47 },
"adders": [{ "name": "Critter guard", "quantity": 1, "total": 450 }]
},
"products": null,
"milestones": { "installScheduled": "2026-10-14", "ntpApproved": "2026-09-28" },
"files": { "siteSurvey": ["https://…"], "planset": [] },
"links": { "project": "https://pipe.solar/dashboard/projects/j57a…", "proposal": null }
}Move a project's stage
/v1/projects/{id}/stageMoves the project into the stage of its ladder with this name, as the Move button does: the stage's checklists, alerts and messages follow. The stage it is already in changes nothing. Only the installer's key moves a project. `{id}` is Pipe's id, or the reference your system holds from the old app (its 10-character UID). A project also answers to its lead's id.
| Field | Type | Required | What it is |
|---|---|---|---|
| stage | string | Yes | The stage's name, as the ladder shows it. Case does not matter. |
| Status | When |
|---|---|
| 200 | `changed` says whether it moved; `stage` is the stage's name. |
| 400 | No stage of that name, or not the installer's key; the message lists the ladder's stages. |
| 404 | No record with this id is yours. |
| 401 | The key is missing, wrong or revoked. |
POST https://staging-api.pipe.solar/v1/projects/{id}/stage
Authorization: Bearer psk_<40 characters>
Content-Type: application/json
{ "stage": "Permitting" }{ "changed": true, "stage": "Permitting" }Set the install date and NTP
/v1/projects/{id}Sets the install date (the project's "Install Scheduled" milestone, which also places it on the Calendar) and whether NTP is granted. An absent field is kept. Only the installer's key changes a project. `{id}` is Pipe's id, or the reference your system holds from the old app (its 10-character UID). A project also answers to its lead's id.
| Field | Type | Required | What it is |
|---|---|---|---|
| installDate | string | No | The day, YYYY-MM-DD, or null to clear it. |
| ntpGranted | boolean | No | Whether notice to proceed is granted. |
| Status | When |
|---|---|
| 200 | `changed` says whether anything changed. |
| 400 | The request is not valid; the message says what to change. |
| 404 | No record with this id is yours. |
| 401 | The key is missing, wrong or revoked. |
PATCH https://staging-api.pipe.solar/v1/projects/{id}
Authorization: Bearer psk_<40 characters>
Content-Type: application/json
{ "installDate": "2026-10-14", "ntpGranted": true }{ "changed": true }Add files to a project
/v1/projects/{id}/filesCopies each file from its address into the project's folder, where the record shows it. Send 1 to 10 at a time. Each address must be public https (no IP address, no private name, no password in it), open the file without signing in, and hold a document, picture, audio or video of 100 MB or less; Pipe keeps its own copy. Sending the same address to the same folder again adds nothing. Only the installer's key adds files. `{id}` is Pipe's id, or the reference your system holds from the old app (its 10-character UID). A project also answers to its lead's id.
| Field | Type | Required | What it is |
|---|---|---|---|
| folder | string | Yes | contract, utilityBill, identification, material, siteSurvey, planset, permit, interconnection, other, homeInsurance, installationPhoto or internal. |
| files | object | Yes | A list of { url, name }: a public https address, and the name to show (the address's own when absent). |
| Status | When |
|---|---|
| 201 | `files` lists each file with its id; `added` is false for an address this folder already holds, so a retry adds nothing twice. |
| 200 | Every address was already in this folder; nothing was added. |
| 400 | The request is not valid; the message says what to change. |
| 404 | No record with this id is yours. |
| 401 | The key is missing, wrong or revoked. |
POST https://staging-api.pipe.solar/v1/projects/{id}/files
Authorization: Bearer psk_<40 characters>
Content-Type: application/json
{
"folder": "permit",
"files": [{ "url": "https://files.example.com/permit-812-main.pdf", "name": "Permit.pdf" }]
}{ "files": [{ "id": "m17c…", "name": "Permit.pdf", "folder": "permit", "added": true }] }List leads
/v1/leadsYour company's leads, solar and HI, newest change first, a page at a time, with `updatedSince` to read only what changed. An installer's key lists every lead sold through it, its dealers' included.
| Field | Type | Required | What it is |
|---|---|---|---|
| limit | number | No | Rows in a page, 1 to 100. 50 when absent. |
| cursor | string | No | The `next` of the page before, URL-encoded, with the same other fields. Absent for the first page. |
| updatedSince | string | No | Only rows changed after this time, ISO 8601 (2026-10-01T00:00:00Z). |
| productLine | string | No | `solar` or `hi`. Both when absent. |
| Status | When |
|---|---|
| 200 | `data` holds the page's leads; `next` is the cursor for the next page, or null at the end. |
| 400 | The request is not valid; the message says what to change. |
| 401 | The key is missing, wrong or revoked. |
GET https://staging-api.pipe.solar/v1/leads Authorization: Bearer psk_<40 characters>
{
"data": [{ "id": "j57a…", "status": "proposal", "updatedAt": "…" }],
"next": null
}Read a lead
/v1/leads/{id}One lead: the customer, the address, the status, the source and the people. `project` names the project once the customer bought. `{id}` is Pipe's id, or the reference your system holds from the old app (its 10-character UID). A project also answers to its lead's id.
| Status | When |
|---|---|
| 200 | The lead. |
| 404 | No record with this id is yours. |
| 401 | The key is missing, wrong or revoked. |
GET https://staging-api.pipe.solar/v1/leads/{id}
Authorization: Bearer psk_<40 characters>{
"id": "j57a…",
"reference": "D4YR1VSK7K",
"productLine": "solar",
"status": "proposal",
"archived": false,
"createdAt": "2026-09-12T15:00:00.000Z",
"updatedAt": "2026-09-19T10:12:00.000Z",
"customer": { "firstName": "Ana", "lastName": "Rivera", "email": "ana@example.com", "phone": "602-555-0100" },
"coBorrower": null,
"address": { "line1": "812 Main St", "line2": null, "city": "Plano", "state": "TX", "postalCode": "75023", "country": "US" },
"utility": { "name": "Oncor", "monthlyBill": 182 },
"source": { "name": "Door knock", "subSource": null },
"externalId": "crm-1042",
"notes": null,
"salesRep": { "name": "Jordan Lee", "email": "jordan@example.com", "phone": null },
"setter": null,
"project": null,
"links": { "record": "https://pipe.solar/dashboard/proposals/j57a…" }
}Read a lead's activity
/v1/leads/{id}/activitiesThe lead's Activity tab, newest first, a page at a time: notes, status and stage changes, files, and the rest. `internal` marks a note only the company's admins see. `{id}` is Pipe's id, or the reference your system holds from the old app (its 10-character UID). A project also answers to its lead's id.
| Field | Type | Required | What it is |
|---|---|---|---|
| limit | number | No | Rows in a page, 1 to 100. 50 when absent. |
| cursor | string | No | The `next` of the page before, URL-encoded, with the same other fields. Absent for the first page. |
| Status | When |
|---|---|
| 200 | `data` holds the page's activities; `next` is the cursor for the next page, or null at the end. |
| 404 | No record with this id is yours. |
| 401 | The key is missing, wrong or revoked. |
GET https://staging-api.pipe.solar/v1/leads/{id}/activities
Authorization: Bearer psk_<40 characters>{
"data": [{ "id": "n2b…", "type": "note", "body": "Customer asked about batteries", "internal": false, "customerFacing": false, "replyTo": null, "author": "Jordan Lee", "createdAt": "2026-09-19T10:12:00.000Z" }],
"next": null
}Change a lead's details
/v1/leads/{id}Changes the customer's details, as the record's edit dialogs do. An absent field is kept; the answer is the lead as it now reads. `{id}` is Pipe's id, or the reference your system holds from the old app (its 10-character UID). A project also answers to its lead's id.
| Field | Type | Required | What it is |
|---|---|---|---|
| firstName | string | No | The customer's first name. |
| lastName | string | No | The customer's last name. |
| string | No | The customer's email. | |
| phone | string | No | The customer's phone. |
| notes | string | No | The lead's notes; also posted to its activity. |
| address | object | No | line1, city, state and postalCode together; line2 and a two-letter country (US when absent) are optional. |
| coBorrower | object | No | firstName, lastName, email, phone; an empty text clears that field. |
| Status | When |
|---|---|
| 200 | The lead, changed. |
| 400 | The request is not valid; the message says what to change. |
| 404 | No record with this id is yours. |
| 401 | The key is missing, wrong or revoked. |
PATCH https://staging-api.pipe.solar/v1/leads/{id}
Authorization: Bearer psk_<40 characters>
Content-Type: application/json
{ "phone": "602-555-0199", "coBorrower": { "firstName": "Luis", "lastName": "Rivera" } }{
"id": "j57a…",
"reference": "D4YR1VSK7K",
"productLine": "solar",
"status": "proposal",
"archived": false,
"createdAt": "2026-09-12T15:00:00.000Z",
"updatedAt": "2026-09-19T10:12:00.000Z",
"customer": { "firstName": "Ana", "lastName": "Rivera", "email": "ana@example.com", "phone": "602-555-0100" },
"coBorrower": null,
"address": { "line1": "812 Main St", "line2": null, "city": "Plano", "state": "TX", "postalCode": "75023", "country": "US" },
"utility": { "name": "Oncor", "monthlyBill": 182 },
"source": { "name": "Door knock", "subSource": null },
"externalId": "crm-1042",
"notes": null,
"salesRep": { "name": "Jordan Lee", "email": "jordan@example.com", "phone": null },
"setter": null,
"project": null,
"links": { "record": "https://pipe.solar/dashboard/proposals/j57a…" }
}List adders and products
/v1/addersThe installer's active adders and HI products, in its own order. For a `perWatt` adder, `unitPrice` is dollars per watt.
| Status | When |
|---|---|
| 200 | `data` holds the adders. |
| 403 | A dealer's key: only the installer's key reads its catalogue. |
| 401 | The key is missing, wrong or revoked. |
GET https://staging-api.pipe.solar/v1/adders Authorization: Bearer psk_<40 characters>
{
"data": [{ "id": "q3d…", "name": "Critter guard", "description": null, "productLine": "solar", "productKind": null, "unit": "fixed", "unitPrice": 450, "tiered": false, "repSponsored": false, "showOnProposal": true, "brand": null, "model": null, "warrantyYears": null }]
}List panels, inverters and batteries
/v1/equipmentThe installer's active equipment. `kind` picks one of the three.
| Field | Type | Required | What it is |
|---|---|---|---|
| kind | string | No | `panel`, `inverter` or `battery`. All three when absent. |
| Status | When |
|---|---|
| 200 | `data` holds the equipment, each with its `kind`. |
| 400 | The request is not valid; the message says what to change. |
| 403 | A dealer's key: only the installer's key reads its catalogue. |
| 401 | The key is missing, wrong or revoked. |
GET https://staging-api.pipe.solar/v1/equipment Authorization: Bearer psk_<40 characters>
{
"data": [{ "id": "r8e…", "kind": "panel", "manufacturer": "REC", "model": "Alpha Pure 410", "watts": 410, "default": true }]
}Create a lead
/v1/leadsSends one homeowner to Pipe as a lead in the key's company. Send the same externalId again and the answer is the lead it already made, so a retried call makes no duplicate.
| Field | Type | Required | What it is |
|---|---|---|---|
| firstName | string | Yes | The homeowner's first name. |
| lastName | string | Yes | The homeowner's last name. |
| address | object | Yes | line1, city, state and postalCode; line2 and a two-letter country (US when absent) are optional. |
| string | No | The homeowner's email. | |
| phone | string | No | The homeowner's phone. |
| salesRepEmail | string | No | An active member of the company. Any other email leaves the rep empty and is listed under unassigned. |
| setterEmail | string | No | The same rule as salesRepEmail, for the setter. |
| notes | string | No | Saved on the lead and posted to its activity. |
| externalId | string | No | Your system's id, at most 128 characters. |
| Status | When |
|---|---|
| 201 | The lead was made. |
| 200 | The externalId was sent before; the answer is that lead. |
| 400 | The body is not valid; the error says why. |
| 401 | The key is missing, wrong or revoked. |
POST https://staging-api.pipe.solar/v1/leads
Authorization: Bearer psk_<40 characters>
Content-Type: application/json
{
"firstName": "Ana",
"lastName": "Rivera",
"address": { "line1": "812 Main St", "city": "Plano", "state": "TX", "postalCode": "75023" },
"externalId": "crm-1042"
}{
"leadId": "k57a…",
"created": true,
"unassigned": []
}