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

CodeStatusWhat it means
unauthorized401No key, or a key that is wrong, revoked, or whose company is not active. Check the key; do not retry.
forbidden403The key is right but the endpoint is not for its company, such as a dealer reading the installer's catalogue.
not_found404No such address, or no record with this id is yours. Another company's record answers the same as a missing one.
invalid400The request is not valid; the message says what to change. Retrying the same request fails the same way.
rate_limited429The key made more than 100 calls in a minute. Wait the seconds `Retry-After` names, then retry.
internal500Pipe'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 externalId you 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

GET/v1/me

Answers the key's name and the company it belongs to. Use it to prove a key works before you build on it.

StatusWhen
200The key works.
401The 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

GET/v1/projects

Your 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.

FieldTypeRequiredWhat it is
limitnumberNoRows in a page, 1 to 100. 50 when absent.
cursorstringNoThe `next` of the page before, URL-encoded, with the same other fields. Absent for the first page.
updatedSincestringNoOnly rows changed after this time, ISO 8601 (2026-10-01T00:00:00Z).
productLinestringNo`solar` or `hi`. Both when absent.
StatusWhen
200`data` holds the page's projects, the core of each; `next` is the cursor for the next page, or null at the end.
400The request is not valid; the message says what to change.
401The 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

GET/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.

StatusWhen
200The project.
404No record with this id is yours.
401The 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

POST/v1/projects/{id}/stage

Moves 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.

FieldTypeRequiredWhat it is
stagestringYesThe stage's name, as the ladder shows it. Case does not matter.
StatusWhen
200`changed` says whether it moved; `stage` is the stage's name.
400No stage of that name, or not the installer's key; the message lists the ladder's stages.
404No record with this id is yours.
401The 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

PATCH/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.

FieldTypeRequiredWhat it is
installDatestringNoThe day, YYYY-MM-DD, or null to clear it.
ntpGrantedbooleanNoWhether notice to proceed is granted.
StatusWhen
200`changed` says whether anything changed.
400The request is not valid; the message says what to change.
404No record with this id is yours.
401The 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

POST/v1/projects/{id}/files

Copies 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.

FieldTypeRequiredWhat it is
folderstringYescontract, utilityBill, identification, material, siteSurvey, planset, permit, interconnection, other, homeInsurance, installationPhoto or internal.
filesobjectYesA list of { url, name }: a public https address, and the name to show (the address's own when absent).
StatusWhen
201`files` lists each file with its id; `added` is false for an address this folder already holds, so a retry adds nothing twice.
200Every address was already in this folder; nothing was added.
400The request is not valid; the message says what to change.
404No record with this id is yours.
401The 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

GET/v1/leads

Your 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.

FieldTypeRequiredWhat it is
limitnumberNoRows in a page, 1 to 100. 50 when absent.
cursorstringNoThe `next` of the page before, URL-encoded, with the same other fields. Absent for the first page.
updatedSincestringNoOnly rows changed after this time, ISO 8601 (2026-10-01T00:00:00Z).
productLinestringNo`solar` or `hi`. Both when absent.
StatusWhen
200`data` holds the page's leads; `next` is the cursor for the next page, or null at the end.
400The request is not valid; the message says what to change.
401The 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

GET/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.

StatusWhen
200The lead.
404No record with this id is yours.
401The 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

GET/v1/leads/{id}/activities

The 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.

FieldTypeRequiredWhat it is
limitnumberNoRows in a page, 1 to 100. 50 when absent.
cursorstringNoThe `next` of the page before, URL-encoded, with the same other fields. Absent for the first page.
StatusWhen
200`data` holds the page's activities; `next` is the cursor for the next page, or null at the end.
404No record with this id is yours.
401The 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

PATCH/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.

FieldTypeRequiredWhat it is
firstNamestringNoThe customer's first name.
lastNamestringNoThe customer's last name.
emailstringNoThe customer's email.
phonestringNoThe customer's phone.
notesstringNoThe lead's notes; also posted to its activity.
addressobjectNoline1, city, state and postalCode together; line2 and a two-letter country (US when absent) are optional.
coBorrowerobjectNofirstName, lastName, email, phone; an empty text clears that field.
StatusWhen
200The lead, changed.
400The request is not valid; the message says what to change.
404No record with this id is yours.
401The 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

GET/v1/adders

The installer's active adders and HI products, in its own order. For a `perWatt` adder, `unitPrice` is dollars per watt.

StatusWhen
200`data` holds the adders.
403A dealer's key: only the installer's key reads its catalogue.
401The 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

GET/v1/equipment

The installer's active equipment. `kind` picks one of the three.

FieldTypeRequiredWhat it is
kindstringNo`panel`, `inverter` or `battery`. All three when absent.
StatusWhen
200`data` holds the equipment, each with its `kind`.
400The request is not valid; the message says what to change.
403A dealer's key: only the installer's key reads its catalogue.
401The 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

POST/v1/leads

Sends 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.

FieldTypeRequiredWhat it is
firstNamestringYesThe homeowner's first name.
lastNamestringYesThe homeowner's last name.
addressobjectYesline1, city, state and postalCode; line2 and a two-letter country (US when absent) are optional.
emailstringNoThe homeowner's email.
phonestringNoThe homeowner's phone.
salesRepEmailstringNoAn active member of the company. Any other email leaves the rep empty and is listed under unassigned.
setterEmailstringNoThe same rule as salesRepEmail, for the setter.
notesstringNoSaved on the lead and posted to its activity.
externalIdstringNoYour system's id, at most 128 characters.
StatusWhen
201The lead was made.
200The externalId was sent before; the answer is that lead.
400The body is not valid; the error says why.
401The 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": []
}