Skip to content

Getting Started

This is a basic description of the steps needed to price out a financial product and continue through contracting.

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.

View Project Template

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.

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.

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;
{
access_token: '...',
expires_in: 86400,
refresh_token: '...',
token_type: 'Bearer'
}

Reference: Login API.

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

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.

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();
{
webhookRegistrationId: '...',
apiKey: '...'
}

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

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();
{
messages: [
{
clientId: '...',
eventType: 'allEvents',
message: 'A new webhook was created.',
},
];
}

Reference: Webhook API

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 externalReference property 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.
  • LseId under the utility is the unique id for a utility from genability
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();
{
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

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.

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();
{
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

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

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

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.

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();
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();
[
...
{
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

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.

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();
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();
[
...
{
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

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.

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();
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();
{
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

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.

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();
{
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

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();
{
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

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

** 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();
[
{
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]);
}

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

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