# Light API Documentation
> Documentation: https://docs.light.dev
> API Reference: https://docs.light.dev/api-reference
> OpenAPI Schema: https://api.light.dev/v1/openapi.json
> Website: https://www.poweredbylight.com
## Overview
Light provides API and infrastructure for companies to offer branded electricity plans without managing operational complexity. This documentation covers the Light API, authentication, enrollment flows, billing, and integration guides.
## Contents
This file contains the complete Light API documentation for use by AI assistants and other automated tools. For the full OpenAPI specification, see https://api.light.dev/v1/openapi.json
---
# enrollment
> https://docs.light.dev/guides/enrollment
# Enrollment
## Intro
Enrollment is where customers sign up for your power plan, typically in a linear flow. Behind the scenes, the flow attaches a [`Location`](/key-concepts#entities) entity to an [`Account`](/key-concepts#entities) entity.
The enrollment experience can be completely customized by using our API with your UI. Light also provides an optional, low-code embedded flow ([prebuilt UI](/prebuilt-ui#embedded-flows)) that can be embedded into your website or mobile app. The embedded flow includes a full enrollment experience that appears in a modal overlaying your experience.
The embedded flow can be a great way to get started with a branded and complete enrollment flow embedded within your existing UI. If you want a more customized experience, we recommend using the API.
Both the API and embedded flow require you to have an app token and create a customer account prior to enrolling it. Refer to the first two steps in [our quickstart](/quickstart) to learn how to do that.
### Prerequisites
- An account or invite to the [Light dashboard](https://dashboard.light.dev/)
- An existing web or native mobile app to add the enrollment experience to
- If not, see [our example app](https://github.com/light-technology/example-app)
## Embedded flow
[image]
The embedded flow lets you skip all of the UI work needed to create an enrollment flow. Only app token authentication and not account tokens is needed for this implementation. Use the `enrollment` scope to surface this specific experience. [Learn more](/prebuilt-ui#tutorial)
[Our quickstart](/quickstart) is a much more in-depth tutorial for implementing the enrollment flow in a pre-built UI.
### Pre-filling address information
If you already have an address for your user and are using an embedded flow, you can set the `preliminary_address` field when creating the `Account`. This will be used as a suggestion when the user is searching for their address. If the location is eligible, then we see around 90% of these addresses end up matching one we have on file. If there is a match we will skip the address search step, while allowing the user to optionally correct the address.
The `preliminary_address` field is essentially passed to the full address search API described lower in the API section. That API does some normalization to the address to try and find an exact match to an address on file with the utility.
## API
Using an API implementation creates a convinent user experience where you can embed the steps within your existing onboarding flows and skip UI steps for customer data you already have.
### 1. (optional) Filter to eligible postal codes
Not every address is eligible for electricity retail choice. So if you already have address information for your users, it can be useful to filter ahead of time
to only show the enrollment flow to users that are likely to be eligible.
First, filter to only states where you're offering a retail electricity product.
The next step we recommend is using our Eligibility API `/v1/account/enroll/eligibility`. This API accepts either a full address or at least a `postal_code` to determine eligibility of a specific location. The response will return a payload like:
```json title="Response format"
{
"eligibility_likelihood": 0,
"utilities": [
{
"display_name": "ONCOR",
"name": "ONCOR"
},
{
"display_name": "CenterPoint",
"name": "CENTERPOINT"
}
]
}
```
The `eligibility_likelihood` here indicates a percentage between 0-100 if the location is eligible in a retail choice electricity plan. We recommend anything above `0` continuing past this check, but anything `0` you could show a screen indicating that their address is not in a retail choice area.
### 2. Identify the `Location`
Not every address is eligible for electricity retail choice, so it is important to be able to search and match the user's address to a valid utility service address registered with the utility. In Texas, valid addresses are identified by their ESI ID, a unique identifier designated by ERCOT.
#### Full address search
If you already have the address for the customer on file, you can attempt to map it to an address on file with the Utility by using this API: `/v1/app/accounts/enroll/full-address-search`.
This API will only return results that appear to be exact matches for the location provided. The API will attempt to do some address normalization in case the address you have uses `123 Main Street Apt 1` and the utility address is `123 Main St #1`, it will still show matches. However it will not fuzzy-match mispellings or incorrect numbers like `124 Main St` or `123 Mian St`. We find that this often has 90+% accuracy with matching eligible addresses, and we are improving it over time to be even higher.
If there is exactly one result returned then it is most likely the correct address for the customer. If you get 0 results, then you may want to offer the type-ahead search or allow the customer to enter their ESIID from a previous utility bill as a fallback. The type-ahead API can help a customer sometimes correct these on their own if their street has multiple names or other common alternative names. It is possible to sometimes receive multiple results if there is more than one electricity meter for the location.
#### Type-ahead search by address
[image]
The `/v1/app/accounts/enroll/address-search` API is best to use if the customer's address is unknown or an exact match was not found. It is meant to be used as a type-ahead search where the user can start typing the beginning of their address and see results.
This API helps you map an address to its corresponding ESI ID by returning a list of addresses (and their ESI IDs) which match the search query. The search uses a fuzzy search algorithm to match the query to the address even if the query is not an exact match. The exact formatting of these addresses may not match that in other data sources like Google Maps since they come directly from the Utilities we work with.
This endpoint is provided as a convenience. However, because it uses fuzzy search and certain addresses can be very similar, it is critical that customers review and confirm the selection of their full address in a separate step and not rely on the ordering of the results from this API.
#### Search by Electric Service Identifier ID (ESI ID)
[image]
You can also identify a Location directly by ESI ID. This is a useful fallback to `/v1/app/accounts/enroll/address-search` when the user has their ESI ID available and wants to enroll in a plan using that ESI ID. They can usually find their ESI ID on a previous electricity bill. You can pass the ESIID collected to `/v1/account/enroll/esi-id-search`, which is useful for validating the ESI ID and returning the address for the user to confirm.
We recommend providing this as a fallback for when the address search does not return any results. This can sometimes be the case if the format of the addresses correlated with the ESI ID is sufficiently different from the address the user is searching for.
### 3. Confirm the address
All of the methods above help to identify the ESI ID for an address. The ESI ID is the unique identifier used by utilities that is needed to enroll a specific electric meter.
Once you have determined ESI ID from one of the search mechanisms above, the user must confirm the address details before proceeding with the enrollment process to prevent faulty or delayed enrollments. The utility account number (ESI ID) should also be shown to the user to confirm that they are enrolling the correct service location. The addresses returned by our APIs will likely look slightly different from those in your system, so even if you already had the correct address for the user make sure you are showing them the address returned by our APIs to confirm. Here is an example of what an integration might show a customer using that address response from the search above.
### 4. Request available plans
[image]
After the service location address is confirmed, the next step is to request available plans for the address. This can be done by using the `/v1/app/accounts/enroll/plans/request` API. This API will return a list of available plans for the address in which the user can enroll.
The plans contain details about all of the sub-components that make up the plan. This includes the energy rate, delivery (TDU) rates, monthly plan charges, term length, and links to the Electricity Facts Label (EFL) and Terms of Service (TOS) documents. The user can review the plans and enroll in them.
The response will also include PDF document links provided for your plans and must be linked to from your app for the user to review as needed. The plan details and variables that are contained within the EFL and TOS documents are also available in the API response for displaying some of the key components to customers in your app (for example the energy rate and term length).
You can also request plans without an ESI ID by calling `/v1/app/accounts/enroll/plans/request`. This API also allows features like customizing the rates per-account.
### 5. Accept the plan
[image]
After the user has reviewed a plan, they can chose to accept the plan and continue in the enrollment process. This can be done by using the `/v1/account/enroll/plans/accept` API. In the visual example above, this would be equivalent to the user clicking the button at the bottom of the screen to accept the plan.
When calling this API, the `terms_accepted` field must be set to true to indicate that the user has accepted the terms of the plan. The `start_date` field must be set to a date within the range of the `earliest_start_date` and `latest_start_date` fields from the plan details.
Calling this API will attach the ESI ID to the Account, creating a Location with a Contract including the terms of the accepted plan. However, the enrollment won't be finalized until the rest of the Enrollments steps below are finalized.
### 6. Verify identity
As part of the enrollment process, users must verify their identity to ensure compliance with regulatory requirements. This step helps protect against fraud and ensures the account is being set up for the correct individual.
The minimum information required to start service includes first and last name, phone number, email, and date of birth.
Set these fields when you create a user's account using `POST /v1/app/accounts`, or collect it later and send via `PATCH /v1/app/accounts/{account_uuid}`.
Once you have set these critical fields, confirm you are ready to check the customer's identity and supply additional information using `PATCH /v1/account/enroll/identity`. The more information provided, the stronger the likelihood of successful identity verification. The actual identity verification process won't start until after the enrollment is submitted.
### 7. Check credit
By default, we will perform a required soft credit check to determine your customer's eligibility. The credit check requires the user's full Social Security Number (SSN) and explicit consent to the terms of the credit check.
Use `POST /v1/account/enroll/credit-check` to submit the SSN to be used for credit check. The credit check will be processed in the background once the enrollment is submitted. If the account doesn't pass the credit check with SSN, then our support team will reach out directly to the customer via email for alternative credit verification methods.
### 8. Add a payment method
[image]
If you are using our built-in Stripe integration, you can access a Stripe public key in our `/v1/app/accounts/{account_uuid}` payload (`app.stripe_public_key`) to use in the various [Stripe Checkout](https://docs.stripe.com/payments/checkout) or [Stripe Elements](https://stripe.com/docs/stripe-js) components that work with your frontend stack. If you are working in the sandbox environment, then this public key will be a Stripe test key with which you can use the standard Stripe test card numbers. You can also save a copy of the public key elsewhere or cache it for future use. It isn't likely to change frequently but may change over the years if needed for security reasons.
In order collect a payment method from the customer, you will first need to create a payment session using the `POST /v1/account/billing/payment-session` API. This API will return a client secret for this account that you can use to create a payment method using [Stripe Elements](https://docs.stripe.com/payments/accept-a-payment?platform=web&ui=elements#add-and-configure-the-elements-provider-to-your-payment-page) along with the Stripe public key.
After the payment method is added to Stripe, you will need to confirm with Light by using the `POST /v1/account/billing/ensure-payment-method-added` API. This API simply takes the `payment_method_id` that is returned by the `Stripe` client SDK. Passing this to us will confirm that the payment method was successfully added through Stripe and sync it with the customer's Account on Light.
The payment method will not be immediately charged through these APIs; rather, it is saved to the customer's account for regular auto-pay invoice billing based on their next billing cycle. By default, payment methods are charged for the amount due on an invoice 16 days after the invoice is generated.
### 9. Confirm enrollment
[image]
After an Account has confirmed their address, accepted a plan, added necessary identity information, supplied SSN for credit check, and added a payment method, the enrollment can be finalized. Finalizing an enrollment is essentially completing a checkout for the enrollment. You can see if all of the steps are completed by checking the `/account` API response enrollment fields. If `enrollment.can_finalize_enrollment` is true, then the enrollment can be finalized. If some of the steps are not completed, then check the other fields in the `enrollment` object to see what is needed.
It can take anywhere from 2 to 72 business hours to complete the enrollment process depending on the utility and the time of day the enrollment was initiated, if the start date selected was the same day. You can be informed as soon as
an account goes live by using [webhooks](/webhooks).
We recommend showing a confirmation screen similar to the one above until the Account's service is active.
Once the utility has accepted the enrollment, the `is_service_active` field will be marked as active in the `/account` API response.
The utility will also supply their usage history for the previous 12 months if available when enabling the service. This information is not received at the exact same time and may be available some hours before or after the service is active.
## Related webhooks
Webhooks allow your application to receive real-time notifications about events that occur within the Light platform. This enables you to build responsive and up-to-date integrations without the need for constant polling. [Learn more](/webhooks)
### `enrollment.plan_requested`
Triggered when a customer requests a plan. This event won't have a location set, but will have an account since the location isn't saved until the plan is accepted.
### `enrollment.plan_accepted`
Triggered when a customer accepts an electricity plan.
### `enrollment.identity_updated`
Triggered when a customer's identity information is updated.
### `enrollment.finalized`
Triggered when an enrollment is finalized.
### `account.payment_method_added`
Triggered when a new payment method is added to an account.
### `location.service_active`
Triggered when electricity service becomes active for a location.
### `location.usage_history_available`
Triggered when usage history becomes available for a location.
---
# billing
> https://docs.light.dev/guides/billing
# Billing
## Intro
The retail-choice electricity market requires a certain method to bill customers that is based on metered usage. This section covers how to retrieve and update payment methods, billing addresses, and access invoice information. Only `AccountToken` [authentication](/authentication) is needed to access billing information.
The billing experience can be completely integrated into your existing billing experience by using our API.
Light also provides two embedded flow scopes ([prebuilt UI](/prebuilt-ui#embedded-flows)) that you can embed in your UI if you prefer.
We recommend using the API experience to create a bespoke billing experience within your existing customer dashboard. The embedded flow is great for shipping a complete billing management experience fast within an existing product.
## Embedded flow
[image]
The embedded flow lets you skip all of the UI work needed to create a billing management flow. Use the `billing` and `update-payment-method` scopes to surface these specific experiences. [Learn more](/prebuilt-ui#tutorial)
- The `billing` scope includes a list of invoices and their details. It also provides a way to manually pre-pay an invoice.
- The `update-payment-method` scope lets you update/change a payment method for an account.
## API
The API requires you retrieve a user's `AccountToken` for the billing experience. Refer to the [authentication docs](/authentication) for more details.
### View payment method
[image]
To see what payment method is currently associated with the account, use the `GET /v1/account/billing/payment-method` API. This will return the payment method, including its last four digits, expiration date, and type.
### Update payment method
Updating a payment method is the same API endpoints as adding a new payment method. View those instructions in the [enrollment guide](/guides/enrollment#8-add-a-payment-method).
### View invoices
To retrieve a list of invoices for the customer, use the `GET /v1/account/billing/invoices` API. This will return a paginated list of invoices with details such as the invoice date, due date, amount, and status. It is recommended to surface if an invoice is paid, voided, or past due. It can also be helpful to surface the payment due date when the auto-pay will attempt to pay the invoice.
For the most simple implementation, you can list these invoices with links to their PDFs. If you want a more branded experience, we recommend using the "View invoice details" API below once an invoice is selected.
### View invoice details
[image]
To get detailed information about a specific invoice, use the `GET /v1/account/billing/invoices/{invoice_number}` API. This will return comprehensive details including line items, usage data, charges, and payment information.
Map over `charges_groups` and their charges to display a detailed breakdown without needing to open the PDF.
### View billing address
You can retrieve the current billing address using the `GET /v1/account/billing/address` API. This will return the current billing address information associated with the customer's account.
The address fields will be populated regardless of whether the `same_as_service_address` field is set to `true` or `false`. If `same_as_service_address` is true, Light populates the address fields with the service address information.
### Update billing address
[image]
Most of the time the billing address will be the same as the service address. In this case, you don't need to set the billing address and the service address will be used.
Use the `PUT /v1/account/billing/address` API to update the billing address.
## Related webhooks
Webhooks allow your application to receive real-time notifications about events that occur within the Light platform. This enables you to build responsive and up-to-date integrations without the need for constant polling. [Learn more](/webhooks)
### `account.payment_method_added`
Triggered when a new payment method is added to an account.
### `account.billing_address_updated`
Triggered when a billing address is updated.
### `account.payment_failed`
Triggered when a payment for an invoice fails for an account.
### `account.payment_successful`
Triggered when a payment for an invoice is successful for an account.
---
# Quickstart
> https://docs.light.dev/quickstart
# Get started with the quickstart
In the quickstart, we'll show you how to:
- Set up a sandbox app and get app credentials in the [Light dashboard](https://dashboard.light.dev/)
- Use the API and an [embedded flow](/prebuilt-ui#embedded-flows) (prebuilt UI) to let customers enroll in your power plan
- Verify customers are able to enroll
### Prerequisites
- An account or invite to the [Light dashboard](https://dashboard.light.dev/)
- An existing web or native mobile app to add the enrollment experience to
- If not, use [our example app](https://github.com/light-technology/example-app)
## 1. Set up a sandbox app and get app credentials in the Light dashboard
1. Log in to the [Light dashboard](https://dashboard.light.dev/)
2. On the Apps page, create a new app that uses the sandbox environment
3. Select the app you just created and go to API Keys from the sidebar
4. Create a new API key and store the `AppToken` somewhere for later
We now have an `AppToken` that can be used on all requests for authentication. An additional customer `AccountToken` is needed to complete actions on behalf of the customer, but that's not needed in this quickstart.
Make sure you keep your `AppToken` a secret and only use it on the server side of your project to hide it from users. See how we use Next.js serverside app requests to protect our `AppToken` in our [our example app](https://github.com/light-technology/example-app/tree/main/app/api). If you want to make requests directly from your client, look into the `AccountToken`. [Learn more](/authentication)
## 2. Create user's Light Account
Before showing the embedded enrollment flow, we need a customer's Light account. Create an account by sending a `POST` to `/v1/app/accounts`:
```javascript
// Create a new customer account
const response = await fetch("https://api.light.dev/v1/app/accounts", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.LIGHT_APP_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
email: "customer@example.com",
first_name: "John",
last_name: "Doe",
}),
});
const account = await response.json();
console.log("Account created:", account.uuid);
```
```python
import requests
import os
response = requests.post(
'https://api.light.dev/v1/app/accounts',
headers={
'Authorization': f'Bearer {os.getenv("LIGHT_APP_TOKEN")}',
'Content-Type': 'application/json'
},
json={
'email': 'customer@example.com',
'first_name': 'John',
'last_name': 'Doe'
}
)
account = response.json()
print(f"Account created: {account['uuid']}")
```
```bash
# Create a new customer account
$ curl -X POST "https://api.light.dev/v1/app/accounts" \
-H "Authorization: Bearer $LIGHT_APP_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"email": "customer@example.com",
"first_name": "John",
"last_name": "Doe"
}'
```
```json title="Example Response"
{
"uuid": "123e4567-e89b-12d3-a456-426614174000",
"email": "customer@example.com",
"first_name": "John",
"last_name": "Doe",
"created_at": "2024-01-15T10:30:00Z"
}
```
See the API reference for the complete response format.
In a real situation, you should save the customer's `account_uuid` to your own database and avoid creating duplicate accounts on Light.
## 3. Launch embedded flow
Then, we need to get a URL that includes the enrollment flow's prebuilt UI. We can use the `uuid` in the last request's response to get a URL specific to your current customer. We place that `uuid` as the `account_uuid` in a POST to `/v1/app/accounts/{account_uuid}/flow-login`:
```javascript
// Get the embedded flow URL for enrollment
const flowResponse = await fetch(
`https://api.light.dev/v1/app/accounts/${account.uuid}/flow-login`,
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.LIGHT_APP_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
scope: "enrollment",
}),
},
);
const flowData = await flowResponse.json();
console.log("Flow URL:", flowData.login_link);
```
```python
# Get the embedded flow URL for enrollment
flow_response = requests.post(
f'https://api.light.dev/v1/app/accounts/{account["uuid"]}/flow-login',
headers={
'Authorization': f'Bearer {os.getenv("LIGHT_APP_TOKEN")}',
'Content-Type': 'application/json'
},
json={
'scope': 'enrollment'
}
)
flow_data = flow_response.json()
print(f"Flow URL: {flow_data['login_link']}")
```
```bash
# Get the embedded flow URL for enrollment
$ curl -X POST "https://api.light.dev/v1/app/accounts/ACCOUNT_UUID/flow-login" \
-H "Authorization: Bearer $LIGHT_APP_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"scope": "enrollment"
}'
```
```json title="Example Response"
{
"login_link": "https://flow.light.dev/login?token=6277d9fb7b76832d1fc7545d4ed649d7",
"scope": "enrollment",
"expires_at": "2024-01-15T11:30:00Z"
}
```
See the API reference for the complete response format.
## 4. Surface enrollment flow at the right moment
The enrollment flow is usually displayed after a user clicks a button or navigates to another page within an iframe or webview. Display the flow in an iframe or webview:
```tsx
interface FlowIframeProps {
url: string;
}
const FlowIframe: React.FC = ({ url }) => {
return (
```
## 5. Close the enrollment flow
We need to close the embedded flow once a user completes or chooses to exit the flow early. Close the embedded flow by listening for the `light-flow-close` event.
Update the component from step 4 to handle the event:
```tsx
interface FlowIframeProps {
url: string;
onClose: () => void;
}
const FlowIframe: React.FC = ({ url, onClose }) => {
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
const eventType = event.data?.type;
if (!eventType) {
return;
}
if (eventType === "light-flow-close") {
onClose();
}
};
window.addEventListener("message", handleMessage);
return () => {
window.removeEventListener("message", handleMessage);
};
}, [onClose]);
return (
```
## 6. Verify customers are able to enroll
### Test the enrollment flow
You can test the enrollment flow using the following data:
- **Email**: Use your own email address or any valid email format
- **Credit Card**: `4242 4242 4242 4242` (Stripe's test card number)
- **Expiry Date**: Any future date (e.g., `12/25`)
- **CVC**: Any 3-digit number (e.g., `123`)
- **Billing Address**: Any valid address information
- **Service Address**: Any valid address information
The sandbox environment uses Stripe's test mode, so no real payments are processed. The credit card number `4242 4242 4242 4242` is Stripe's standard test card that will always succeed for testing purposes.
### Check enrollment
Now we can check to see if the customer is enrolled in the dashboard:
1. Log in to the [Light dashboard](https://dashboard.light.dev/)
2. On the Apps page, go to the app you previously created
3. Go to the Accounts page and select Enrolled
You should see an enrolled customer's details.
## Next steps
Congratulations on completing the Light quickstart! You've completed all of the steps necessary to enroll new customers in a power plan. To launch the experience, just repeat the first step to make a live app and use that `AppToken`.
There are other experiences that are important to your customer's journey. Explore our guides to learn about billing, documents, renewal, service, and usage tracking.
## FAQs
### What's the difference with a sandbox and live app?
Sandbox apps are safe to use in development and testing. No power plans will actually take affect. So you are free to enroll, change details, and cancel service with sandbox credentials.
### What if I don't have an account on the Light dashboard?
The Light dashboard currently requires that a teammate or Light invites you to join. [Reach out to us](https://www.poweredbylight.com/#contact) if you're interested in partnering with Light.
---
# documents
> https://docs.light.dev/guides/documents
# Documents
## Intro
After enrollment, you will need to provide the customer with important documents related to their service. These documents include the Electricity Facts Label (EFL), Terms of Service (TOS), and Your Rights as a Customer (YRAC).
These documents are also included in the `/v1/app/accounts/enroll/plans/request` response payload when the customer is enrolling in a plan.
## Embedded flow
[image]
The embedded flow ([prebuilt UI](/prebuilt-ui#embedded-flows)) lets you skip all of the UI work needed to share documents. Use the `documents` scope to surface this specific experience. [Learn more](/prebuilt-ui#tutorial)
## API
The API requires you retrieve a user's `AccountToken` for the document experience. Refer to the [authentication docs](/authentication) for more details.
[image]
### Get active documents for a location
To retrieve a list of active documents for a specific location, use the `GET /v1/account/locations/{location_uuid}/documents` API.
### Get all documents for a location
To retrieve a list of all documents (including historical) related to a specific service location, use the `GET /v1/account/locations/{location_uuid}/all-documents` API. This includes past documents as well as queued documents that are scheduled to start in the future (for example, a renewal)
---
# key-concepts
> https://docs.light.dev/key-concepts
# Key concepts
The Light API revolves around several key entities that work together to enable seamless management of electricity services for consumers.
## Entities
### Apps
Each `App` corresponds to a single application or integration built on the Light platform. Apps have their own set of accounts, data, and configuration. You can create your own Sandbox Apps in the Light dashboard. Once you have an App, you can configure it, download an App Token, and configure webhooks.
Currently, Light assists with the creation of Live Apps. When you are ready to go live, we will work with you to help create a Live App and ensure it is ready to launch.
### Accounts
An `Account` represents a prospective or active electricity consumer. Creating an Account requires minimal information such as an email address and name. Once an Account is created, it can be used to enroll or manage the consumer's electricity service for a service location.
### Location
A `Location` is attached to an `Account` and corresponds to a physical service address where an electricity consumer resides with an electricity meter. The process of creating or attaching the Location to an Account is called "Enrollment." This process includes verifying eligibility, retrieving available plans, and accepting an offered `Plan`.
If users in your system only have one location or property, then you can have one `Account` per user. However, if users in your system may have multiple locations, then you will likely want to create an `Account` per location.
### Plan groups
A `Plan Group` represents a type of electricity plan available to consumers. Plan groups include feature details such as name, base fees, solar buyback programs, and commission.
The `Plan Group` does not include specific rates, as those will vary depending on the location of the customer and the fluctuations in the market pricing day-to-day.
### Plans
A `Plan` represents an individual instance of a plan group with specific rates and terms at that point in time. The `Plan` is dependent on location of the customer, and the time being requested. Until accepted by a customer, Plans will expire as market rates change.
`Plan`s include the specific rates that would show on an Electricity Facts Label (EFL), as well as the Terms of Service (TOS) and Your Rights as a Customer (YRAC).
You can also generate custom rates per `Account`, overriding some components like term length and commission. This can be useful for offering the same overall `Plan` in different term lengths or prices depending on your business needs.
### Invoices and billing
An `Invoice` represents the billing statement for electricity consumption over a specific period. Invoices ensure regulatory compliance and provide detailed billing information to consumers. The Light API offers endpoints to access itemized invoice details and download invoice documents.
## API structure
Light API organizes endpoints into groups to make it easier to interact with these entities. These groups include the following:
- **`/app` endpoints**: Used for managing and interacting with Apps, including creating and managing accounts, generating tokens, and retrieving app-specific data. These all use `AppToken` authentication.
- **`/app/accounts/enroll` endpoints**: Used for privileged enrollment operations with `AppToken` authentication, including address search, eligibility checks, requesting plans with custom rates, and accessing plan benchmarks.
- **`/account` endpoints**: Used for managing individual consumer Accounts. Endpoints use `AccountToken` authentication.
- **`/account/enroll` endpoints**: Used for the enrollment process with `AccountToken` authentication, including address search, identity verification, credit checks, and plan acceptance.
- **`/account/billing` endpoints**: Used for billing-related tasks, such as creating payment sessions, retrieving invoices, updating payment methods, and managing billing addresses.
- **`/account/locations/{location_uuid}` endpoints**: Used for managing locations, including retrieving plan details, accessing service documents, viewing usage data (meter reads, monthly, daily, and interval usage), and managing service cancellation.
- **`/account/sandbox` endpoints**: Used for testing and simulating account states in the sandbox environment.
## Rate limiting
The Light API implements rate limiting to prevent API overload and ensure fair usage across all clients. Rate limits are applied based on the authentication method used.
Rate limits use a per-second sliding window. When a request exceeds the rate limit, the API returns an HTTP `429 (Too Many Requests)` status code.
#### AppToken API (`/app/*`)
Requests authenticated with an AppToken are rate limited to **30 requests per second** per app.
#### AccountToken API (`/account/*`)
Requests authenticated with an AccountToken are rate limited to **10 requests per second** per account.
## Account statuses
In Account-related APIs, the response will commonly include `status` and `status_reason` fields. The possible values for these fields are given here with the corresponding explanations.
Status
Status Reason
Description
`not_enrolled`
`unresolved`
Customer account has just been created and has no status yet.
`not_enrolled`
`plan_not_accepted`
Customer has not accepted a plan to enroll.
`not_enrolled`
`missing_address`
Customer has not provided an address to enroll.
`not_enrolled`
`missing_identity`
Customer has not provided additional information such as a birth date or social security number to enroll.
`not_enrolled`
`missing_payment_method`
Customer has not provided a payment method to enroll.
`reviewing`
`verification_pending`
Customer's enrollment is being verified by us.
`reviewing`
`credit_check_pending`
Customer's credit check is being verified by us.
`reviewing`
`utility_submitted`
Customer's enrollment has been submitted to their local utility for processing.
`needs_attention`
`credit_not_found`
Customer has been emailed to provide additional credit proof points.
`needs_attention`
`credit_declined`
Customer's credit score was low and they have been emailed to provide additional credit proof points.
`needs_attention`
`credit_check_frozen`
Customer has been emailed to let us know when they unfreeze their credit so we can complete a credit check.
`needs_attention`
`credit_check_error`
Credit provider was unable to check credit for this customer, but it might be available in the future. Sit tight while we continue checking.
`needs_attention`
`enrollment_blocked_switch_hold`
Customer's existing or prior REP has placed a hold on their account, usually due to being on a deferred payment plan. Unable to proceed until hold lifted.
`needs_attention`
`enrollment_blocked_verification_hold`
Light has flagged this account for a manual review.
`needs_attention`
`enrollment_blocked_deactivated`
Customer's meter exists in utility system but is not active. Needs further investigation.
`needs_attention`
`utility_blocked`
We submitted an enrollment to the utility system but there was a problem with it. Needs further investigation.
`ready`
`service_pending`
Customer's service is scheduled to start on a future date.
`active`
`active`
Customer's service with us is active.
`cancelled`
`cancelled_pre_start`
Customer's service was cancelled before service was active.
`cancelled`
`expire_pre_active`
Customer's rates expired before they completed verification.
`cancelled`
`cancelled_post_start`
Customer's service was cancelled after service was active.
---
# authentication
> https://docs.light.dev/authentication
# Authentication
There are two different types of authentication for the Light APIs. Both types utilize tokens passed as `Bearer` tokens in the `Authorization` header of HTTP requests. For example, call `GET /v1/app/accounts` with your app token to see your app accounts.
```javascript
const response = await fetch("https://api.light.dev/v1/app/accounts", {
headers: {
Authorization: `Bearer ${process.env.LIGHT_APP_TOKEN}`,
},
});
```
```python
import requests
import os
response = requests.get(
'https://api.light.dev/v1/app/accounts',
headers={
'Authorization': f'Bearer {os.getenv("LIGHT_APP_TOKEN")}'
}
)
```
```bash
curl --request GET --url https://api.light.dev/v1/app/accounts \
--header "Authorization: Bearer $LIGHT_APP_TOKEN"
```
## App Tokens vs Account Tokens
The first type of auth token is an `AppToken`, which is a long-lived token that is generated per-application that you build on Light. These tokens can be used for privileged APIs such as creating new `Account`s for your `App`, editing sensitive details about those accounts such as their verified email address, and generating `AccountToken`s to interact further with the accounts. `AppToken`s should be kept secret and only used from a secure environment like your backend server.
The second type of token is an `AccountToken`. These are tokens that you generate using your `AppToken` for a specific account (i.e. user of your `App`). `AccountToken`s are not long-lived and will expire after 60 minutes (or at the `expires_at` timestamp given in the `POST /app/accounts/{uuid}/token` API response). These tokens can only access information or affect a single user account of your app and can be used either from a backend server or a frontend client. `AccountTokens` can be used from a frontend client to speed up and simplify your integration by avoiding proxying all requests through your server. However, you can also use these from your server if you prefer.
## Using Account Tokens
In order to use `AccountToken`s from your frontend client, you will need to provide a way for your client to fetch a new `AccountToken` periodically as it expires. We would recommend a pattern similar to the following, where you can serve a Light `AccountToken` for a given user on your platform via API.
### 1. Backend API to generate Account Tokens
This example includes optional caching of the `AccountToken` to avoid making unnecessary calls to the API. However if you are caching on your frontend client too, then you can likely skip caching on your backend.
```javascript
app.post("/energy/token", async (req, res) => {
// Save a Light account UUID to your user model once you have created
// a Light account for the user. (Or if your users can have multiple locations,
// then save the Light account UUID for each location.)
const lightAccountUuid = req.user.lightAccountUuid;
if (!lightAccountUuid) {
throw new Error("Light account not created");
}
// Cache the per-account tokens since they are valid for 1 hour
const cacheKey = `energy-token-${lightAccountUuid}`;
const cachedToken = await cache.get(cacheKey);
if (cachedToken) {
return res.json(cachedToken);
}
// POST /app/accounts/{light_account_uuid}/token
const response = await fetch(
`https://api.light.dev/v1/app/accounts/${lightAccountUuid}/token`,
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.LIGHT_APP_TOKEN}`,
"Content-Type": "application/json",
},
},
);
const tokenData = await response.json();
// cache for 50 minutes giving a 10 minute buffer before expiration
await cache.set(cacheKey, tokenData, 50 * 60);
res.json(tokenData);
});
```
```python
@api.post("/energy/token")
def get_energy_token(request):
# Save a Light account UUID to your user model once you have created
# a Light account for the user. (Or if your users can have multiple locations,
# then save the Light account UUID for each location.)
light_account_uuid = request.user.light_account_uuid
if light_account_uuid is None:
raise Exception("Light account not created")
# Cache the per-account tokens since they are valid for 1 hour
cache_key = f"energy-token-{light_account_uuid}"
token = cache.get(cache_key)
if token:
return token
api = LightServerAPI()
# POST /app/accounts/{light_account_uuid}/token
response = api.create_energy_token(light_account_uuid)
# cache for 50 minutes giving a 10 minute buffer before expiration
cache.set(cache_key, response, 50 * 60)
return response
```
```bash
# Example cURL request to generate account token
curl -X POST "https://api.light.dev/v1/app/accounts/ACCOUNT_UUID/token" \
-H "Authorization: Bearer $LIGHT_APP_TOKEN" \
-H "Content-Type: application/json"
```
```json title="Example Response"
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expires_at": "2024-01-15T11:30:00Z"
}
```
See the API reference for the complete response format.
### 2. Frontend client to use Account Tokens
Your front-end would then call the above `/energy/token` endpoint that you implemented and then use the resulting Account Token for calling Light API endpoints. You may also want to cache Account Tokens on the client to avoid having to call your `/energy/token` endpoint before each client-side call to the Light API. For example, in JavaScript you could have an API helper like this:
```tsx
interface EnergyToken {
token: string | null;
expiry: string | null;
}
const useEnergyToken = () => {
const [energyToken, setEnergyToken] = useState({
token: null,
expiry: null,
});
const getCachedToken = (): string | null => {
const { token, expiry } = energyToken;
return token && expiry && new Date() < new Date(expiry) ? token : null;
};
const fetchToken = async (): Promise => {
const response = await fetch("/energy/token", {
method: "POST",
credentials: "include",
});
if (!response.ok) throw new Error("Failed to fetch token");
const { token, expires_at } = await response.json();
// cache the token and expiration date
setEnergyToken({ token, expiry: expires_at });
return token;
};
const getToken = async (): Promise => {
return getCachedToken() || (await fetchToken());
};
return { getToken };
};
export default useEnergyToken;
```
```javascript
const energyToken = {
token: null,
expiry: null,
};
// Function to get the cached token if it exists and is not expired
function getCachedToken() {
const { token, expiry } = energyToken;
return token && expiry && new Date() < new Date(expiry) ? token : null;
}
// Function to fetch a new token from the backend
async function fetchToken() {
const response = await fetch("/energy/token", {
method: "POST",
credentials: "include",
});
if (!response.ok) throw new Error("Failed to fetch token");
const { token, expires_at } = await response.json();
// cache the token and expiration date
energyToken.token = token;
energyToken.expiry = expires_at;
return token;
}
// Function to get the token, using cache if available
export async function getToken() {
return getCachedToken() || fetchToken();
}
```
### 3. Example client usage
You can then use the above helper in your frontend API calls like this:
```tsx
// Example component that uses the account token
const AccountDataComponent = () => {
const { getToken } = useEnergyToken();
const fetchAccountData = async () => {
try {
const token = await getToken();
const response = await fetch("https://api.light.dev/v1/account", {
method: "GET",
headers: { Authorization: `Bearer ${token}` },
});
if (!response.ok) throw new Error("Failed to fetch account data");
const data = await response.json();
console.log("Account data:", data);
} catch (error) {
console.error("Error fetching account data:", error);
}
};
return (
);
};
export default AccountDataComponent;
```
```javascript
// Example function to perform an API request using the account token
async function fetchAccountData() {
const token = await getToken();
const response = await fetch("https://api.light.dev/v1/account", {
method: "GET",
headers: { Authorization: `Bearer ${token}` },
});
if (!response.ok) throw new Error("Failed to fetch account data");
const data = await response.json();
console.log("Account data:", data);
}
```
---
# service
> https://docs.light.dev/guides/service
# Service
## Intro
It's important to manage energy service and get customers the right help when they need it. Only `AccountToken` [authentication](/authentication) is needed to access service information.
## API
The API requires you retrieve a user's `AccountToken` for the billing experience. Refer to the [authentication docs](/authentication) for more details.
### Get support info
[image]
Light provides support contact information for locations via the `GET /v1/account/locations/{location_uuid}/support-info` API. This API includes contacts for your customer support team, powered by Light, as well as the TDU for the location.
A Transmission and Distribution Utility (TDU) is the regulated company that owns and maintains poles, wires, and meters in the customer's area. A customer may want their contact information if there is an outage with their service. If calls do make it to the Light support team, they will also be able to get the TDU contact information.
### Cancel service
If a customer signs up for a different retail energy provider at a location you service, Light will receive the cancellation through the Utility and process it for you. Typically a customer does not need to directly cancel their service unless they are moving. In the event they are moving, they can contact support through phone or email to get help with their move.
## Related webhooks
Webhooks allow your application to receive real-time notifications about events that occur within the Light platform. This enables you to build responsive and up-to-date integrations without the need for constant polling. [Learn more](/webhooks)
### `location.service_active`
Triggered when electricity service becomes active for a location.
### `location.service_canceled`
Triggered when electricity service is canceled for a location.
---
# versioning
> https://docs.light.dev/versioning
# Versioning
Our API is designed to evolve over time, and we are committed to maintaining a stable and predictable experience for our developers. To achieve this, we use versioning to manage changes and ensure compatibility.
## Current version
The current version of our API is prefixed with `v1/`. For example: `https://api.light.dev/v1/app/accounts/{account_uuid}`.
## Approach
Here’s how we handle different types of changes to the API:
### Backward-compatible changes
These include adding new endpoints, new fields to existing responses, or new optional parameters to existing requests. To ensure your integration remains stable, please design it to gracefully handle unknown keys in API responses. These changes will be made within the current version (`v1`) and will not require any modification from your side.
### Backward-incompatible changes
These changes include removing or renaming existing endpoints, changing the structure of responses or requests, or any changes that would require modifications to your integration. When such changes are necessary, they will be introduced in a new version (e.g., `v2`, or another version header to be designed in the future). We will provide detailed documentation and migration guides to help you transition to new versions as needed.
Sometimes we will introduce APIs tagged with "Preview" or "Beta" which may be subject to backwards incompatible changes without up-reving the version. In these scenarios we will try our best to inform customers using them of changes and give time to switch-over.
## Deprecation policy
When we release a new version, the previous version will enter a deprecation period. During this period:
- The deprecated version will continue to function as expected.
- We will provide detailed documentation and migration guides to help you transition to the new version.
- The deprecation period will last for at least 6 months or until confirmed that your code is not using the deprecated functionality, giving you ample time to make necessary adjustments.
## Notifications and support
- **Notifications**: We will notify you of any upcoming changes via our developer portal, email updates, and in our documentation.
- **Support**: Our team is available to assist you with any questions or issues you encounter during the transition to a new version.
We are committed to making API updates as seamless as possible and will provide the resources and support you need to keep your integration running smoothly, while also taking advantage of new features and improvements.
---
# prebuilt-ui
> https://docs.light.dev/prebuilt-ui
# Prebuilt UI
While you can build every experience on your own using our API, Light offers several prebuilt UI solutions that let you easily integrate with the Light platform:
- **Embedded flows** - Want to keep customers in your experience, but skip building the UI? Embedded flows are a low-code experience where you embed an iframe or webview directly within your product.
- **No-code web app** - A standalone web app with a complete account management portal ready to go, using your brand. Link your customers to the portal and they'll use their email address to log in.
- **No-code flows** - These can handle discrete transactional interactions with customers, for example when customers are renewing plans. Customers are sent a Light-hosted flow with your brand.
## Experience availability
Use any combination of the API and our prebuilt UI solutions to create your Light integration. Since the prebuilt UI solutions use the same APIs available to you, you can always start with a prebuilt UI and migrate to a direct API integration with custom UI as needed.
| **Experience** | **Embedded flows** | **No-code web app** | **No-code flows** |
| -------------- | ------------------ | ------------------- | ----------------- |
| Enrollment | **X** | | |
| Billing | **X** | **X** | |
| Documents | **X** | **X** | |
| Service | | **X** | |
| Energy usage | | **X** | |
| Renewals | **X** | | **X** |
## Brand configuration
The prebuilt UIs have a default look and feel that is designed to be easy to use and integrate. They can be minimally configured with things like logos and company name to match your brand. However, you can completely customize your design by building on top of our APIs directly.
## Embedded flows
[image]
Light offers optional embedded flows that let you integrate the Light platform into your website or mobile app using a webview or iframe, simplifying your integration and saving development time.
You can launch embedded flows from your application with various scopes to handle parts of the customer journey like Enrollment or Billing. Each embedded flow is built entirely on the same APIs you can use directly.
### Scopes
Each embedded flow has a specific scope that determines the flow that will be launched.
The following scopes are currently supported, but more scopes will be added in the future.
Reach out to us if you have a specific use case that you would like to see supported.
Scopes:
- `enrollment` - Launches the embedded flow with the enrollment experience. [Preview](/guides/enrollment#embedded-flow)
- `update-payment-method` - Launches the embedded flow for a user to update their payment method. [Preview](/guides/billing#embedded-flows)
- `billing` - Launches the embedded flow with the user's invoices. [Preview](/guides/billing#embedded-flows)
- `documents` - Launches the embedded flow with the user's documents. [Preview](/guides/documents#embedded-flow)
### Tutorial
#### 1. Get an embedded flow link
When you launch an embedded flow, you'll launch it for a specific Account. Use the `uuid` of the Account as the `account_uuid` in a POST to `/v1/app/accounts/{account_uuid}/flow-login?scope=enrollment`:
```javascript
// Get the embedded flow URL for enrollment
const response = await fetch(
`https://api.light.dev/v1/app/accounts/${accountUuid}/flow-login`,
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.LIGHT_APP_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
scope: "enrollment",
}),
},
);
const flowData = await response.json();
console.log("Flow URL:", flowData.login_link);
```
```python
import requests
import os
# Get the embedded flow URL for enrollment
response = requests.post(
f'https://api.light.dev/v1/app/accounts/{account_uuid}/flow-login',
headers={
'Authorization': f'Bearer {os.getenv("LIGHT_APP_TOKEN")}',
'Content-Type': 'application/json'
},
json={
'scope': 'enrollment'
}
)
flow_data = response.json()
print(f"Flow URL: {flow_data['login_link']}")
```
```bash
# Get the embedded flow URL for enrollment
curl -X POST "https://api.light.dev/v1/app/accounts/ACCOUNT_UUID/flow-login" \
-H "Authorization: Bearer $LIGHT_APP_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"scope": "enrollment"
}'
```
```json title="Response format"
{
"login_link": "https://flow.light.dev/login?token=6277d9fb7b76832d1fc7545d4ed649d7",
"scope": "enrollment",
"expires_at": "2025-09-30T14:30:00.753Z"
}
```
The `login_link` returned will be a pre-authenticated flow link that can be used to launch an iframe or webview. The `expires_at` indicates when the pre-authenticated token included in the `login_link` will expire.
#### 2. Surface the embedded flow at the right moment
The embedded flow is usually displayed inside an iframe or webview after a user clicks a button or navigates to another page.
For end users to successfully navigate an embedded flow, no other UI elements should visually appear on top of the iframe or webview. Ensure other UI elements, such as nav bars, have a lower `z-index` and are not absolute-positioned on top.
```html
```
```tsx
interface FlowIframeProps {
url: string;
}
const FlowIframe: React.FC = ({ url }) => {
return (
);
};
export default FlowIframe;
```
#### 3. Close the enrollment flow
You'll need to close the embedded flow once a user chooses to exit the flow. Close the embedded flow by listening for the `light-flow-close` event emitted by the embedded flow.
```html
Light Enrollment Flow
```
```tsx
interface FlowIframeProps {
url: string;
onClose: () => void;
}
const FlowIframe: React.FC = ({ url, onClose }) => {
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
const eventType = event.data?.type;
if (!eventType) {
return;
}
if (eventType === "light-flow-close") {
onClose();
}
};
window.addEventListener("message", handleMessage);
return () => {
window.removeEventListener("message", handleMessage);
};
}, [onClose]);
return (
);
};
export default FlowIframe;
```
## No-code web app
On a deadline? We also have a prebuilt account portal app that you can white-label with no-code to get started, which handles everything your customers need after enrollment. Reach out for us to get started.
## No-code flows
These are prebuilt UI flows hosted by Light to handle discrete interactions with customers. These can be used for out-of-band experiences like renewals to save you time of building for all paths at launch. They are typically launched via a pre-authenticated email link, providing a quick and branded interaction for the customer.
### Scopes
The following scopes are currently supported, but more scopes will be added in the future.
Reach out to us if you have a specific use case that you would like to see supported.
Scopes:
- `renewal` - Launches the no-code flow with the renewal experience.
---
# usage
> https://docs.light.dev/guides/usage
# Energy usage
## Intro
Helping track customer's energy usage can help them understand their bills and project future usage. Only `AccountToken` [authentication](/authentication) is needed to access usage information.
## API
Our usage API endpoints differ based on the time interval you want to receive usage data. The API requires `AccountToken` (refer to the [authentication docs](/authentication) for more details).
Depending on the customer's utility, we may be able to access up to two years of historical data for a single customer. The usage data often lags by a day or two.
[image]
The usage data returned by these APIs are estimated for analyzing usage and information, but may not align exactly to the meter readings. This is due to multiple factors including that the meter read happens and different times of the day than the interval boundaries.
All of these APIs provide both import and export usage data. Import refers to energy consumed from the grid, while export refers to energy sent back to the grid (in the case of solar panels or battery storage).
### Get monthly usage
This endpoint returns an entire year of usage separated by month. In addition, it provides links to the previous and next year of usage if applicable. For this interval, use the `GET /v1/account/locations/{location_uuid}/usage/monthly` API.
### Get daily usage
This endpoint returns an entire month of usage separated by day. In addition, it provides links to the previous and next month of usage if applicable. For this interval, use the `GET /v1/account/locations/{location_uuid}/usage/daily` API.
### Get 15-minute interval usage
This endpoint returns an entire month of usage separated by 15-minute intervals. In addition, it provides links to the previous and next month of usage if applicable. For this interval, use the `GET /v1/account/locations/{location_uuid}/usage/intervals` API.
Most days have 96 intervals (four 15-minute intervals per hour for a 24 hour day). However, for locations participating in daylight savings time, this could be 92 intervals (spring forward) or 100 intervals (fall back) on some days.
We receive this data one day at a time, so there will never be a partial day represented.
---
# renewal
> https://docs.light.dev/guides/renewal
# Renewal
## Intro
Once a customer is approaching the end of their contract, they will be sent reminder emails to accept a renewal contract. They will be switched to a month-to-month plan if a renewal plan is not accepted by the original contract expiration date.
By default, you can rely on the no-code flow for renewals to handle the experience and regulatory needs.
We send renewal emails once a customer is eligible for renewal (90 days out from expiration), with links to hosted no-code flows. These flows provide a branded experience where customers can see a renewal rate and accept it to start once their current term ends.
The default renewal terms use the same pricing model as is used for new customers. You can work with our team to adjust default overrides like extending term length or changing pricing on renewal if desired.
Most partners start with our default renewal behavior at launch. Once you have a good handle on your new customer flows, you may consider building a custom renewal flow if you need specific control over the renewal experience or want to integrate it directly into your app.
## Prebuilt UI
### No-code flow
The default renewal experience uses our [no-code flow](/prebuilt-ui#no-code-flows) hosted by Light. When a customer is eligible for renewal, they receive an email with a link to a branded flow where they can:
1. Review their renewal rate and plan details
2. See when their current contract ends
3. Accept the renewal to start service on the next day after expiration
This happens automatically without any implementation required from your side.
### Custom embedded flow
If you want to host the renewal flow in your own app, we offer a prebuilt UI embedded flow using the same renewal interface. This lets you keep customers within your domain and app experience but without having to build the entire flow from scratch.
To use this option:
1. Implement the prebuilt UI with the `renewal` scope in your app
2. Let us know where you've set up the flow
3. We'll update our renewal emails to link to your app instead of the default hosted flow
[Learn more about prebuilt UI](/prebuilt-ui#launching-a-prebuilt-ui-flow)
## API
If you want to customize your renewal process beyond the prebuilt flows, you can use our API directly. The prebuilt flows are built on top of our public API, so you can achieve the same behavior with a native implementation.
The API requires you retrieve a user's `AccountToken` for the renewal experience. Refer to the [authentication docs](/authentication) for more details.
### Request renewal plans
To show customers their available renewal plans, use the `POST /v1/account/locations/{location_uuid}/renewal-plans/request` API. This returns the renewal plans available for an active service location.
The response includes plan details like rates, term length, and start dates. For typical renewals, the `earliest_start_date` and `latest_start_date` will be the same date (the day after the current contract ends).
### Accept renewal plan
Once a customer selects a renewal plan, use the `POST /v1/account/locations/{location_uuid}/renewal-plans/accept` API to finalize their selection.
You'll need to pass:
- `plan_uuid`: The unique identifier for the selected renewal plan
- `service_start_date`: The start date (typically the `earliest_start_date` from the request)
- `terms_accepted`: Must be `true` to confirm the customer accepted the terms
### Customizing renewal rates
If you want to offer different rates or plans to customers on renewal, you can use the general enrollment plans API instead. Call `POST /v1/app/accounts/enroll/plans/request` to request a custom plan, then accept the renewal using the renewal accept API with the `service_start_date` set to the day after their current contract ends.
This approach gives you full control over what plans and rates you offer at renewal time.
## Related webhooks
Webhooks allow your application to receive real-time notifications about events that occur within the Light platform. This enables you to build responsive and up-to-date integrations without the need for constant polling. [Learn more](/webhooks)
### `location.renewal_plan_requested`
Triggered when a customer requests renewal plans for their location.
### `location.renewal_plan_accepted`
Triggered when a customer accepts a renewal plan for their location.
---
# webhooks
> https://docs.light.dev/webhooks
# Webhooks
Webhooks allow your application to receive real-time notifications about events that occur within the Light platform. This enables you to build responsive and up-to-date integrations without the need for constant polling.
It may not be necessary to use webhooks depending on how you plan to use the Light API. However, they can be useful when you use the Account API or Pre-built UI to talk directly from your client application to the Light API, to notify your backend of key events.
## Setting up webhooks
You can set up webhooks for your app from the Developer settings in your dashboard. When setting up a webhook, you'll provide us with an API endpoint URL where we'll send webhook events. You'll also receive a secret key that you can use to verify the authenticity of webhook events as every webhook event is signed with an HMAC signature.
## Supported webhooks
Currently, we support the following webhook events:
- `enrollment.plan_requested`: Triggered when a customer requests a plan
- This event won't have a location set, but will have an account since the location isn't saved until the plan is accepted.
- `enrollment.plan_accepted`: Triggered when a customer accepts an electricity plan
- `enrollment.identity_updated`: Triggered when a customer's identity information is updated
- `enrollment.finalized`: Triggered when an enrollment is finalized
- `account.payment_method_added`: Triggered when a new payment method is added to an account
- `account.billing_address_updated`: Triggered when a billing address is updated
- `account.payment_failed`: Triggered when a payment for an invoice fails for an account
- `account.payment_successful`: Triggered when a payment for an invoice is successful for an account
- `location.service_active`: Triggered when electricity service becomes active for a location
- `location.service_canceled`: Triggered when electricity service is canceled for a location
- `location.usage_history_available`: Triggered when usage history becomes available for a location
- `location.renewal_plan_requested`: Triggered when a customer requests renewal plans for their location
- `location.renewal_plan_accepted`: Triggered when a customer accepts a renewal plan for their location
- `comparison_invoice.processed`: Triggered when a comparison invoice is done being processed (either successfully or unsuccessfully)
## Webhook payload format
Webhook payloads are sent as JSON in the body of a POST request. The root keys of all webhooks will look the same, but the data object will vary depending on the event type.
```json title="Example payload"
{
"uuid": "123e4567-e89b-12d3-a456-426614174000",
"created_at": "2023-04-01T12:00:00Z",
"api_version": "v1",
"event": "enrollment.plan_accepted",
"data": {
"account": {
"uuid": "98765432-e89b-12d3-a456-426614174000",
"first_name": "John",
"last_name": "Doe",
"email": "john.doe@example.com"
// ... other account details
},
"location": {
"uuid": "12365432-e89b-1a23-a356-323634134300",
"service_start_date": "2025-05-03",
"final_service_date": null
// ... other location details
}
}
}
```
The "enrollment._" and "account._" events will contain an `account` object mirroring that of the Account API response.
The "location._" events will always contain a `location` object and an `account` object. The "account._" events will always include an `account` objects, and sometimes a `location` if one is set for the `account`.
## Webhook delivery
We attempt to deliver webhooks up to 3 times total with a backoff if we don't receive a 2xx response from your server. All attempts will be made within a 10-minute window. If all attempts fail, the webhook will be discarded.
Webhook attempts will timeout after 10 seconds. So if you need to do significant processing or network calls after receiving a webhook we would recommend to return a 200 and spawn an asynchronous job on your system for the extended processing.
## Best practices
1. We recommend using webhooks as triggers to fetch data that would be eventually fetched either way, and not as a source of truth. Webhooks can be missed or delivered out of order due to a variety of issues from the sending or receiving side.
2. Design your system to handle occasional missing webhooks or duplicate deliveries, even though these scenarios are rare.
3. Use webhook signatures for securing your webhook endpoint.
4. The webhook request will timeout after 10 seconds, so ensure your webhook endpoint can respond within that time frame. We recommend processing the webhook payload asynchronously to avoid blocking the response.
5. For webhooks types that you don't care about, respond with a 200 status code to acknowledge receipt of the webhook. Responding with a 2xx status code will prevent the webhook from being retried. If we receive an abundance of 4xx or 5xx responses from an endpoint, we may stop sending webhooks to your endpoint in the future.
## Webhook signatures
To ensure the security and authenticity of webhook payloads, we sign each webhook using HMAC with SHA-256. The signature is included in the `Light-Signature-v1` header of the webhook request.
The header value is in the format: `{timestamp}.{hmac}`
Where:
- `timestamp` is the Unix timestamp to the nearest second when the webhook was sent
- `hmac` is the hexadecimal representation of the HMAC-SHA256 digest
To verify the signature, you should:
1. Split the `Light-Signature-v1` header value into its timestamp and hmac components.
2. Verify that the timestamp is not too old (e.g., older than 1 hour) to limit replay attack exposure.
3. Generate your own HMAC-SHA256 digest using the timestamp and the raw payload of the webhook separated by a period.
4. Compare the resulting digest with the hmac provided in the header using a secure, constant-time comparison method.
Here's an example implementation of signature verification:
```javascript
const express = require("express");
const crypto = require("crypto");
const app = express();
// Middleware to verify webhook signatures
function verifyWebhookSignature(req, res, next) {
const signature = req.headers["light-signature-v1"];
const payload = JSON.stringify(req.body);
if (
!signature ||
!compareSignature(process.env.WEBHOOK_SECRET, payload, signature)
) {
return res.status(401).send("Invalid signature");
}
next();
}
// Webhook endpoint
app.post(
"/webhook",
express.raw({ type: "application/json" }),
verifyWebhookSignature,
(req, res) => {
const payload = JSON.parse(req.body);
// Process the webhook payload
console.log("Received webhook:", payload.event);
// Handle different webhook events
switch (payload.event) {
case "enrollment.plan_accepted":
// Handle plan acceptance
break;
case "account.payment_method_added":
// Handle payment method addition
break;
case "location.service_active":
// Handle service activation
break;
}
res.status(200).send("OK");
},
);
function compareSignature(signingSecret, payload, providedSignature) {
const split = providedSignature.split(".");
if (split.length !== 2) {
return false;
}
const [timestamp, hmacDigest] = split;
if (parseInt(timestamp) < Math.floor(Date.now() / 1000) - 3600) {
return false;
}
const message = `${timestamp}.${payload}`;
const expectedDigest = crypto
.createHmac("sha256", signingSecret)
.update(message)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(hmacDigest, "hex"),
Buffer.from(expectedDigest, "hex"),
);
}
```
```python
from flask import Flask, request, jsonify
import hashlib
import hmac
import time
app = Flask(__name__)
def compare_signature(signing_secret, payload, provided_signature):
"""Compare the provided Light-Signature-v1 signature to the computed expected signature."""
split = provided_signature.split('.')
if len(split) != 2:
return False
timestamp, hmac_digest = split
# if the timestamp is older than 1 hour, reject the request to avoid replay attacks
if int(timestamp) < time.time() - 3600:
return False
message = f"{timestamp}.{payload}"
expected_digest = hmac.new(
signing_secret.encode('utf-8'),
message.encode('utf-8'),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(hmac_digest, expected_digest)
@app.route('/webhook', methods=['POST'])
def webhook():
signature = request.headers.get('Light-Signature-v1')
payload = request.get_data(as_text=True)
if not signature or not compare_signature(
app.config['WEBHOOK_SECRET'], payload, signature
):
return jsonify({'error': 'Invalid signature'}), 401
data = request.get_json()
# Process the webhook payload
print(f"Received webhook: {data['event']}")
# Handle different webhook events
if data['event'] == 'enrollment.plan_accepted':
# Handle plan acceptance
pass
elif data['event'] == 'account.payment_method_added':
# Handle payment method addition
pass
elif data['event'] == 'location.service_active':
# Handle service activation
pass
return jsonify({'status': 'OK'}), 200
```
By implementing signature verification, you can ensure that the webhooks you receive are genuine and have not been tampered with.
---