Getting Started
This is a basic description of the steps needed to price out a financial product and continue through contracting.
Project Template
Section titled “Project Template”If you are using TypeScript we recommend downloading our project template which makes it quick and easy to get up and running on this API. This template includes a generated client SDK with rich typing to make navigating and calling the API simple.
Set up
Section titled “Set up”This guide will assume an existing Machine to Machine user. For more information about users see the User Guide.
You will need an access token to begin. This token should be cached for the lifetime of that
token. This can be determined by either using the expires_in property in the return payload or the exp claim
in the token itself. Once the token is expired you should call the login endpoint to retrieve a new token.
Rate Limits
Section titled “Rate Limits”The Finance API is rate limited to 1200 requests per minute, calculated per minute.
If you exceed this limit you will receive a 429 Too Many Requests response.
You can check the X-RateLimit-Limit and X-RateLimit-Remaining headers in a response to see your current status.
Request
Section titled “Request”import fetch from 'node-fetch';
const response = await fetch(`${envBaseUrl}/api/auth/login`, { method: 'POST', body: JSON.stringify({ username: 'string', password: 'string', }), headers: { 'Content-Type': 'application/json' },});const data = await response.json();const accessToken = data.access_token;Result
Section titled “Result”{ access_token: '...', expires_in: 86400, refresh_token: '...', token_type: 'Bearer'}Reference: Login API.
Subscribe to events
Section titled “Subscribe to events”While not required it is recommended that you use webhooks to be notified of events through the system and eliminate the need to poll for updates. For more information, including event types and payload examples, see the Webhooks Guide
Create a registration
Section titled “Create a registration”The first step is to register a client, providing a useful friendly name and a base or host URL that will be the root of all subscription calls. The call will return the registrationID which will be used to create subscriptions and a unique API key that can be used to authenticate calls you receive from the system.
Request
Section titled “Request”import fetch from 'node-fetch';
const response = await fetch(`${envBaseUrl}/api/webhooks/registrations`, { method: 'POST', body: JSON.stringify({ name: 'Crown Pawn', hostUrl: 'https://webhook.site/4f6a441c-bd9d-4ef5-85c2-d662963d562f', includeVisibleOrgEvents: true, }), headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', },});
const { webhookRegistrationId, apiKey } = await response.json();Result
Section titled “Result”{ webhookRegistrationId: '...', apiKey: '...'}Create a Subscription
Section titled “Create a Subscription”Once you have a registration you can create subscriptions to events. The endpointUrl is the path that will be appended to the hostUrl provided in the registration.
A list of event types can be found in the Webhooks Guide
Request
Section titled “Request”import fetch from 'node-fetch';
const response = await fetch(`${envBaseUrl}/api/webhooks/registrations/${webhookRegistrationId}/subscriptions`, { method: 'POST', body: JSON.stringify({ endpointUrl: '/all', httpMethod: 'POST', eventType: 'allEvents', }), headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', },});
const subscription = await response.json();Result
Section titled “Result”{ messages: [ { clientId: '...', eventType: 'allEvents', message: 'A new webhook was created.', }, ];}Reference: Webhook API
Create an account
Section titled “Create an account”Accounts are the fundamental resource in the system, they are the combination of a finance application / decision and a physical location. While there are routes, for example the products route, that do not require the creation of an account, using the route that requires an account provides more accurate information. For example, the product offering can and does change based on attributes of the account, so the routes requiring an account will provide more accurate information about than the account-less route.
A few items of note while creating an account
- The top level
externalReferenceproperty can be used to store identifiers to external resources, such as an ID in the callers system. This property is not used internally and is simply stored as a string and returned with the account, as well as being included in the payload of all outbound webhooks. LseIdunder the utility is the unique id for a utility from genability
Request
Section titled “Request”import fetch from 'node-fetch';
const response = await fetch(`${envBaseUrl}/api/accounts`, { method: 'POST', body: JSON.stringify({ externalReference: '1994', friendlyName: 'Maynard', address: { address1: '20933 Roscoe Blvd', city: 'Canoga Park', state: 'MA', zip: '02779', }, applicants: [ { type: 'primary', firstName: 'Maynard', lastName: 'Crown', phoneNumber: '5555550001', address: { address1: '20933 Roscoe Blvd', city: 'Canoga Park', state: 'MA', zip: '02779', }, }, ], coordinates: { lat: -71.0952744, lon: 41.8718583, }, language: 'English', utility: { lseId: 2437, tariffId: 3428079, rate: 0.349085473480621, }, systemDetails: { systemFirstYearProductionKwh: 5105.649683000001, systemSizeKw: 4, panelCount: 10, }, salesRepName: 'Quentin Tarantino', }), headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', },});const { accountId } = await response.json();Result
Section titled “Result”{ id: '64a822dd2238174defd6b09c', organizationId: 'crown-pawn-shop', externalReference: '1994', friendlyName: 'Maynard', address: { address1: '20933 Roscoe Blvd', city: 'Canoga Park', state: 'MA', zip: '02779' }, applicants: [ { type: 'primary', firstName: 'Maynard', lastName: 'Crown', phoneNumber: '5555550001', address: { address1: '20933 Roscoe Blvd', city: 'Canoga Park', state: 'MA', zip: '02779' } } ], language: 'English', utility: { lseId: 2437, tariffId: 3428079, rate: 0.349085473480621, utilityName: 'Eversource Energy (Formerly NSTAR Electric Company)' }, systemDetails: { systemFirstYearProductionKwh: 5105.649683000001, systemSizeKw: 4, panelCount: 10 }, salesRepName: 'Quentin Tarantino', status: '1 - Created'}Reference: Account API
Create a credit application
Section titled “Create a credit application”At any point after account creation a credit application can be created. However, the most logical time is after an end user wants to move forward, and thus a quote has been created.
Note: The list of disclosures can be fetched from the disclosures endpoint. These disclosures can also be added during the account creation process.
Request
Section titled “Request”import fetch from 'node-fetch';
const response = await fetch(`${envBaseUrl}/api/accounts/${accountId}/applications`, { method: 'POST', body: JSON.stringify({ applicants: [ { type: 'primary', firstName: 'Maynard', lastName: 'Crown', phoneNumber: '5555550001', address: { address1: '20933 Roscoe Blvd', city: 'Canoga Park', state: 'MA', zip: '02779', }, ssn: '500101005', disclosures: [ { id: '63d18cf156f4a0c0da78aa46', type: 'creditApplication', version: 'v0.1', accepted: true, }, ], }, ], }), headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', },});
const application = await response.json();Result
Section titled “Result”{ id: '64a847282238174defd6b167', applicants: [ { type: 'primary', firstName: 'Maynard', lastName: 'Crown', phoneNumber: '5555550001', address: [Object], disclosures: [Array] } ], propertyOwnershipStatus: 'unverified', status: 'approvedWithStipulations', stipulations: [ { stipulationType: 'achRequirement', description: 'ACH payment information is required', isSatisfied: false, requiresReview: false }, { stipulationType: 'identityVerification', description: 'Verification of identity is required', isSatisfied: false, requiresReview: false }, { stipulationType: 'titleVerification', description: 'Verification of property title is required', isSatisfied: false, requiresReview: false } ], creditExpiryDate: '2023-10-05T17:11:04.630Z'}Reference: Credit Application API
Create a system design
Section titled “Create a system design”Core Concept: Creating a system design provides light reach with all of the PV system information needed to create a quote and have knowledge about the system being sold & installed. These fields are optional so a system design can be created with the most basic information (systemFirstYearProductionKwh and systemSizeKw) and as more information is learned about the system, new system designs can be created.
There can only be one active system design per account. This means every time a system design is created, it becomes the current design.
System design information will need to be updated depending on what has been quoted (ex. if a battery is added, the system design will need storage information).
import fetch from 'node-fetch';
const response = await fetch(`${envBaseUrl}/api/accounts/${accountId}/system-design`, { method: 'POST', body: JSON.stringify({ systemFirstYearProductionKwh: 12000, systemSizeKw: 12, inverters: [ { manufacturer: 'Enphase', model: 'IQ7-60-2-US', count: 1, }, ], panelManufacturer: 'Canadian Solar'', panelModel: 'CS3N-380MS', totalPanelCount: 20, mountingType: 'roof', mountingManufacturer: 'IronRidge', }), headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', },});Reference: Create System Design
Get System Design
Section titled “Get System Design”Returns the current system design for the account.
import fetch from 'node-fetch';
const response = await fetch(`${envBaseUrl}/api/accounts/${accountId}/system-design/current`, { method: 'GET', headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', },});
const design = await response.json();Result
{ systemFirstYearProductionKwh: 12000, systemSizeKw: 12, inverters: [ { manufacturer: 'Enphase', model: 'IQ7-60-2-US', count: 1, }, ], panelManufacturer: 'Canadian Solar'', panelModel: 'CS3N-380MS', totalPanelCount: 20, mountingType: 'roof', mountingManufacturer: 'IronRidge', isCurrent: true,}Reference: Get System Design
Get pricing
Section titled “Get pricing”Core Concept: Requesting pricing in the context of an account allows the system to only provide pricing and products that are available to that account. For example, an account in the state of MA will not be returned pricing for products that are only offered in CA. While it’s preferred to fetch pricing with the context of an account if there is a need to see all available products use the finance products endpoint
Once an account is created, pricing can be fetched. The API is designed to support many different user experiences. For example, a list of products can be inferred by mapping over the returned array of pricing. This data is what should be used to drive a “proposal” experience.
Adder Support: If the solar project has certain adders, you can specify them as part of your pricing request as query string parameters, which should open up additional pricing bands (i.e. higher kWh rates) to cover the cost of the adder. The adder options and bands can vary by market, but in general the following adders are supported Arbitrage Battery, Backup Battery, Electrical Upgrade (e.g. MPU), Roof Upgrade. To include the additional adder bands, just include the query string parameters outlined in the api reference below.
Request (without adders)
Section titled “Request (without adders)”import fetch from 'node-fetch';
const response = await fetch(`${envBaseUrl}/api/v2/accounts/${accountId}/pricing`, { method: 'GET', headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', },});const pricing = await response.json();Request (with adders)
Section titled “Request (with adders)”import fetch from 'node-fetch';
const response = await fetch(`${envBaseUrl}/api/v2/accounts/${accountId}/pricing?electricalUpgradeIncluded=true`, { method: 'GET', headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', },});const pricing = await response.json();Result
Section titled “Result”[ ... { productId: '646519316a223e9302c35744', type: 'ppa', name: 'LightReach Predictable PPA - 0.99%', description: 'Fixed cost PPA Lease', lseId: 2437, utilityName: 'Eversource Energy - East', state: 'MA', escalationRate: 0.0099, kwhRate: 0.16, ppwRate: 2.85, monthlyPayments: [ { year: 1, monthlyPayment: 68.08 }, ... { year: 25, monthlyPayment: 76.46 } ] } ...]Reference: Pricing API
Get estimated pricing
Section titled “Get estimated pricing”Even if you have not created a LightReach account for your customer, you can still retrieve pricing to drive your UI, the only difference is that you have to provide all of the information necessary to determine pricing via the body of the post to the estimated pricing endpoint. As with the pricing route above, adders can also be specified in the payload. By default this endpoint retrieves our default pricing. Once you have created an account you may optionally specify an accountId in the request body to retrieve the best available pricing for the customer.
Request (without adders)
Section titled “Request (without adders)”import fetch from 'node-fetch';
const response = await fetch(`${envBaseUrl}/api/v2/accounts/estimatedPricing`, { method: 'POST', body: JSON.stringify({ lseId: 2437, state: MA, systemSizeKw: 9.3, systemFirstYearProductionKwh: 10755, }), headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', },});const pricing = await response.json();Request (with adders)
Section titled “Request (with adders)”import fetch from 'node-fetch';
const response = await fetch(`${envBaseUrl}/api/v2/accounts/estimatedPricing`, { method: 'POST', body: JSON.stringify({ lseId: 2437, state: MA, systemSizeKw: 9.3, systemFirstYearProductionKwh: 10755, electricalUpgradeIncluded: true, }), headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', },});const pricing = await response.json();Result
Section titled “Result”[ ... { productId: '646519316a223e9302c35744', type: 'ppa', name: LightReach Predictable PPA - 0.99%', description: 'Fixed cost PPA Lease', lseId: 2437, utilityName: 'Eversource Energy - East', state: 'MA', escalationRate: 0.0099, kwhRate: 0.16, ppwRate: 2.85, monthlyPayments: [ { year: 1, monthlyPayment: 68.08 }, ... { year: 25, monthlyPayment: 76.46 } ] } ...]Reference: Estimated Pricing API
Create a quote
Section titled “Create a quote”Once the end user has selected a financial product and wants to move forward, a quote should be created. You may think of a quote as an “order” you are telling the system the financial product and pricing that the end user has selected. There can only be one active quote at a time. If any adders are included, you should specify them using the optional fields in the request body. In addition, if you are including a backup battery, you must both specify backupBatteryIncluded = true and submit your price for the backup battery in the field backupBatteryCost.
Request (without adders)
Section titled “Request (without adders)”import fetch from 'node-fetch';
const response = await fetch(`${envBaseUrl}/api/v2/accounts/${accountId}/quotes`, { method: 'POST', body: JSON.stringify({ productId: '646519316a223e9302c35744', }), headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', },});
const quote = await response.json();Request (with adders)
Section titled “Request (with adders)”import fetch from 'node-fetch';
const response = await fetch(`${envBaseUrl}/api/v2/accounts/${accountId}/quotes`, { method: 'POST', body: JSON.stringify({ productId: '646519316a223e9302c35744', electricalUpgradeIncluded: true, }), headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', },});
const quote = await response.json();Result
Section titled “Result”{ id: '64a8394a449225deda077ef9', accountId: '64a822dd2238174defd6b09c', productId: '646519316a223e9302c35744', externalReference: '', systemFirstYearProductionKwh: 5105.649683000001, systemSizeKw: 4, kwhPrice: 0.16, totalSystemCost: 11400, pricePerWatt: 2.85, preConAdderCost: 0, systemPricingDetails: [ { year: 1, monthlyPayment: 68.08, estimatedAnnualProduction: 5106, guaranteedAnnualProduction: 4595, yearlyCost: 816.96, kwhRate: 0.16 }, ... { year: 25, monthlyPayment: 76.43, estimatedAnnualProduction: 4527, guaranteedAnnualProduction: 4074, yearlyCost: 917.16, kwhRate: 0.2026 } ], totalAmountPaid: 21650.28, status: 'active'}Reference: Quote API
Send contract
Section titled “Send contract”After a quote has been created and a credit application has been approved (or approved with stipulations), a contract can be sent to the end user for signature.
Request
Section titled “Request”import fetch from 'node-fetch';
const response = await fetch(`${envBaseUrl}/api/accounts/${accountId}/contracts/current/send`, { method: 'POST', headers: { Authorization: `Bearer ${accessToken}`, },});
const contract = await response.json();Result
Section titled “Result”{ id: '64a8394a449225deda077efa', accountId: '64a822dd2238174defd6b09c', quoteId: '64a8394a449225deda077ef9', externalReference: '', contractDocuments: [ { references: [{ name: 'accountId', value: '64a822dd2238174defd6b09c' }], type: 'esignature', provider: 'docusign', providerData: { templateId: '17c62d0a-098f-4a98-98aa-2c71d2630e38', envelopeId: '00270084-bb84-4384-a69c-78460dd370f0' }, status: 'created' } ], status: 'sent'}Reference: Contract API
Create a contract signing link
Section titled “Create a contract signing link”Once a contract has been sent, a signing link can be created for the current sent contract. This URL can be used to access the sent contract directly for signing. Upon sending the contract, the account holder will still receive an email with a link to sign the document. Creating a signing link here does not affect the integrity of that link.
Note: This link expires after 5 minutes and is single use, so this link should be created on demand and not stored or sent out.
import fetch from 'node-fetch';
const response = await fetch(`${envBaseUrl}/api/accounts/${accountId}/contracts/current/signing-link`, { method: 'POST', headers: { Authorization: `Bearer ${accessToken}`, }, body: JSON.stringify({ returnUrl: 'https://returnURL.com', }),});
const signingLink = await response.json();Result
Section titled “Result”{ url: 'https://docusign.com/link-to-sign-document';}Reference: Contract API
Checking if a new contract needs to be signed
Section titled “Checking if a new contract needs to be signed”After a contract is signed, there can be many changes that occur prior to installing the system. There are certain scenarios where a new contract is needed and other times the current signed contract covers what those changes are.
To check to see if the account requires a new contract to be signed:
import fetch from 'node-fetch';
const response = await fetch(`${envBaseUrl}/api/accounts/${accountId}/validate-change`, { method: 'POST', body: JSON.stringify({ production: 12000, adders: ['arbitrageBattery'], }), headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', },});Result
{ "status": "Requires new contract", "rangeValidationResult": { "valid": false, "percentDifference": 20, "difference": 5000 }, "reasons": [ { "reason": "Annual production size outside of tolerance", "field": "production" } ]}Reference: Validate Change API
Submitting an Install Package
Section titled “Submitting an Install Package”After Notice To Proceed (M0) has been reached and the project has been installed, an Install Package needs to be compiled and submitted to complete the Install (M1) milestone.
The Install Package is made up of the following requirements:
- PV System Details
- Installer information (for IL only)
- Inverter (and battery if applicable) monitoring site Id
- Adder manufacturer and model (if applicable)
- Design tool
- Permit Submitted Date, Permit Approval Date, Install Date
- Utility Tariff Id
- Install Photos and documents:
- Shade Report
- Plan Set
- Production Model
- Permit
- Incentive documentation (if applicable)
- Utility Bill
- Project Site
- Roof
- Electrical
- Storage (if applicable)
- System Commissioning
Uploading Install Photos / Documents
Section titled “Uploading Install Photos / Documents”** NOTE: ** Document size is currently limited to 32 mb.
To upload install photos / documents :
import fetch from 'node-fetch';
const response = await fetch(`${envBaseUrl}/api/accounts/${accountId}/documents`, { method: 'POST', headers: { Authorization: `Bearer ${accessToken}`, }, body: fileData, // see example of how to construct fileData below});
const document = await response.json();Result
Section titled “Result”[ { id: '64a8394a449225deda077efa', accountId: '64a822dd2238174defd6b09c', status: 'pending', archived: false, type: 'roof', files: [ { originalName: 'roof.pdf', contentType: 'application/pdf', md5Hash: 'GxO72ZJpEVrGWE1AYKF96g==', sizeKB: 4319.5, viewUrls: [ { url: '/api/accounts/${accountId}/documents/${id}/files/${filePath}', type: 'original', }, ], }, ], meta: { createdAt: '2024-01-05T17:11:04.630Z', updatedAt: '2024-01-05T17:11:04.630Z', }, },];Reference: Document API
Each document type requires it’s own POST request to upload the documents.
The document types are as follows:
- Shade Report -
shadeReport - Plan Set -
planSet - Production Model -
productionModel - Permit -
permit - Incentive documentation -
incentives - Utility Bill -
utilityBill - Project Site -
projectSite - Roof -
roof - Electrical -
electrical - Storage (if applicable) -
storage - System Commissioning -
systemCommissioning
The body of the POST request should contain multipart form data with the files. Example:
const fileData = new FormData();fileData.append('type', 'roof');for (let i = 0; i < files.length; i++) { fileData.append(`files[${i}]`, files[i]);}Submitting the Install Package
Section titled “Submitting the Install Package”Approved Vendor List: The PV System data for the system design should align with our Approved Vendor List (AVL). These values can also be fetched via the Approved Vendor List API.
Design Tools: The accepted values for design tools can be fetched from the Design Tools API.
Once all the documents are uploaded, the install package is ready to be submitted.
To submit the install package:
import fetch from 'node-fetch';
const response = await fetch(`${envBaseUrl}/api/v2/accounts/${accountId}/install-package/submit`, { method: 'POST', body: JSON.stringify({ systemDesign: { panelManufacturer: 'Canadian Solar', panelModel: 'CS3N-380MS', totalPanelCount: 20, inverters: [ { manufacturer: 'Enphase', model: 'IQ7-60-2-US', count: 1, }, ], mountingType: 'roof', mountingManufacturer: 'IronRidge', }, // this is only applicable to accounts in IL installer: { legalName: 'Installer Name', iccDocketNumber: '123456', qualifiedPersonOnsiteFirstName: 'Bob', qualifiedPersonOnsiteLastName: 'Builder', phoneNumber: '5555555555', address: '123 Building St', city: 'Chicago', state: 'IL', }, monitoring: { monitoringSiteId: '123456', noInverterConsumptionCT: false, batteryMonitoringSiteId: '123456', // if a battery is installed noBatteryConsumptionCT: false, }, // only required if adders exist on the quote adders: [ { type: 'arbitrageBattery', quantity: 1, model: 'BAT-10K1PS0B-02', manufacturer: 'SolarEdge', }, ], designTool: 'Aurora', permitSubmittedDate: { palmetto: new Date(), }, permitApprovedDate: { palmetto: new Date(), }, installScheduledDate: { palmetto: new Date(), }, utility: { tariffId: 99999, }, }), headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', },});Reference: Submit Install Package API
Saving the Install Package
Section titled “Saving the Install Package”If you do not have all the install documents or information ready to submit, you can save progress on the install package. The request looks almost identical to /submit except all of the fields are optional.
To save the install package:
import fetch from 'node-fetch';
const response = await fetch(`${envBaseUrl}/api/v2/accounts/${accountId}/install-package/submit`, { method: 'POST', body: JSON.stringify({ systemDesign: { panelManufacturer: 'Canadian Solar', panelModel: 'CS3N-380MS', totalPanelCount: 20, }, monitoring: { monitoringSiteId: '123456', noInverterConsumptionCT: false, batteryMonitoringSiteId: '123456', // if a battery is installed noBatteryConsumptionCT: false, }, designTool: 'Aurora', }), headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', },});Reference: Save Install Package API