# 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
# Basic 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. Create an account
This guide covers enrollments where the customer's service start date is within 30 days (or within your configured start date window). If the start date is further out, use the [client agent enrollment](/guides/client-agent-enrollment) flow instead.
#### (optional) Check location eligibility first
If you already have the customer's address or postal code, you can check eligibility before creating an account to avoid enrolling customers in areas that aren't served. Use `POST /v1/app/accounts/enroll/eligibility` with your app token (no account required).
```json title="Request"
{
"postal_code": "78701"
}
```
```json title="Response"
{
"eligibility_likelihood": 85,
"utilities": [
{ "display_name": "ONCOR", "name": "ONCOR" }
]
}
```
`eligibility_likelihood` is a score from 0–100. If it returns `0`, the address is not in a retail choice area and you can skip creating an account entirely.
Before starting the enrollment flow, create a customer account using `POST /v1/app/accounts`. At minimum you'll need the customer's email address, but you can also supply name, phone, and date of birth upfront to skip collecting it later in the identity step.
```json title="Minimum required fields"
{
"email": "jane.smith@example.com"
}
```
The response includes an `account_uuid` and an `account_token`. The account token is a short-lived token scoped to this specific account and is required for the enrollment steps that follow.
### 2. (optional) Register power of attorney
If you hold limited power of attorney (POA) for the customer and intend to accept a plan on their behalf, use `POST /v1/app/accounts/{account_uuid}/record-client-agent` to register that authorization. This call must happen before you call `plans/accept` on the customer's behalf, but it can otherwise be made at any point after account creation, before or after identity, credit, or payment steps.
Note that recording POA does not require you to accept the plan on the customer's behalf. Even with POA registered, the customer can still go through the regular flow and accept a plan themselves.
If the customer's start date is more than 30 days out, see the [client agent enrollment](/guides/client-agent-enrollment) guide for the full flow.
### 3. 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.
### 4. 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.
### 5. 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.
### 6. Accept the plan
[image]
After the user has reviewed a plan, they can choose 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 enrollment steps below are finalized.
You can only call this API on behalf of a customer if you have registered power of attorney for their account in step 2 above. Without it, plan acceptance must be completed by the customer directly.
### 7. 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.
### 8. 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.
### 9. 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.
### 10. Finalize enrollment
The last required API call in the enrollment flow is `POST /v1/account/enroll/finalize`. A confirmation screen in your app does not complete enrollment. You must call this endpoint to submit the enrollment to Light.
Use the customer's account token (the same Bearer token used for other `/v1/account/enroll/*` endpoints). The request body is optional. For a standard enrollment, send an empty JSON object or omit the body.
```bash title="Example request"
curl -X POST "https://api.light.dev/v1/account/enroll/finalize" \
-H "Authorization: Bearer $ACCOUNT_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
```
On success, the API returns `200` with an empty JSON object. If prerequisites are not met, it returns `422` with `"Cannot finalize enrollment"`.
#### When to call it
Call finalize when the customer has completed the prior steps and is ready to submit their enrollment. Before calling, fetch `GET /v1/account` and confirm `enrollment.can_finalize_enrollment` is `true`.
That field is `true` only when all of the following are satisfied:
| Requirement | Check on `enrollment` |
| --- | --- |
| A plan has been accepted | `has_accepted_plan` is `true` |
| A default payment method is on file | `has_payment_method` is `true` |
| Identity information has been submitted | `is_identity_verified` is `true` |
| Enrollment has not already been submitted | `is_enrollment_finalized` is `false` |
You should also have confirmed the service address and submitted the credit check (steps 3 and 8) before finalizing, even though those steps are not reflected in `can_finalize_enrollment`. If `can_finalize_enrollment` is `false`, use the fields above to see which step is still incomplete.
#### What it does
This endpoint submits the enrollment to Light. It sets `enrollment.is_enrollment_finalized` to `true`, populates `enrollment.finalized_at`, and fires the `enrollment.finalized` [webhook](#enrollmentfinalized). Identity verification and the credit check run in the background after submission. The customer receives enrollment confirmation email.
Until this call succeeds, the enrollment stays pending.
Finalizing does not mean electricity service is active. Utility switch processing typically takes 2 to 72 business hours depending on the utility and whether the selected start date is the same day. You can be informed as soon as an account goes live by using [webhooks](/webhooks).
#### After finalization
[image]
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, `enrollment.is_service_active` will be `true` in the `GET /v1/account` 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.
---
# client-agent-enrollment
> https://docs.light.dev/guides/client-agent-enrollment
# Client agent enrollment
Client agent enrollment is an extension of the [basic enrollment flow](/guides/enrollment) that lets you accept an electricity plan on a customer's behalf, using a limited **power of attorney (POA)**. This is useful when you need to coordinate enrollment with an external event (like a hardware installation) rather than having the customer act at a specific moment.
## When to use a client agent enrollment
Two common scenarios where client agent enrollment improves the experience:
#### Hardware installation workflows
For solar, battery, or EV charger installations, electricity service needs to start when hardware comes online. That happens after installation and permission to operate (PTO) is granted, which can be weeks or months after the customer signed up. Rather than contacting the customer again at that moment and asking them to enroll, you can collect their authorization upfront and activate service yourself when the installation is complete.
#### Streamlined logistics
If your onboarding already collects everything needed (identity, payment, and the customer's agreement), adding a separate electricity enrollment step creates unnecessary friction. With client agent authorization, you can complete enrollment in the background as part of your existing workflow, without requiring the customer to take another action.
In both cases, the customer consents upfront and you act when the time is right.
Using client agent authorization requires explicit, informed consent from the customer. Before using this flow, customers must clearly understand:
- That you will enroll them in an electricity plan on their behalf
- Approximately when you will do so (e.g., "when your installation is complete")
- What plan they will be enrolled in
You are responsible for obtaining a signed **limited power of attorney agreement** and retaining it. The `client_agent_accepted` and `client_agent_accepted_at` fields in the API are your attestation that you hold valid authorization.
---
## How it differs from basic enrollment
The client agent flow follows the [basic enrollment](/guides/enrollment) steps with two differences:
1. **You record POA after creating the account.** Call `POST /v1/app/accounts/{account_uuid}/record-client-agent` to register your authorization. This must happen before you call `plans/accept` on the customer's behalf, but it can otherwise be made at any point after account creation.
2. **You accept the plan, not the customer.** When the service start date is known and approaching, you request plans and call `plans/accept` using the account token. The customer does not need to take any action at this point.
Everything else (identity verification, credit check, payment method, address) works exactly as described in the [basic enrollment guide](/guides/enrollment) and can be completed in any order.
---
### 1. Create the account
This step is the same as [basic enrollment step 1](/guides/enrollment#1-create-an-account). Include any customer information you already have (name, phone, date of birth) to prevent additional API calls and reduce what the customer needs to provide themselves.
### 2. Record power of attorney
Call `POST /v1/app/accounts/{account_uuid}/record-client-agent` to record the customer's authorization. This must be done before you call `plans/accept` on the customer's behalf, but can otherwise happen at any point after account creation.
**Fields:**
| Field | Required | Description |
|---|---|---|
| `client_agent_accepted_at` | No | ISO 8601 datetime when the customer granted authorization (e.g. `"2026-03-01T14:30:00Z"`). Defaults to now if omitted. |
```javascript
const response = await fetch(
`https://api.light.dev/v1/app/accounts/${accountUuid}/record-client-agent`,
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.LIGHT_APP_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
client_agent_accepted_at: "2026-03-01T14:30:00Z",
}),
},
);
```
```python
response = requests.post(
f"https://api.light.dev/v1/app/accounts/{account_uuid}/record-client-agent",
headers={
"Authorization": f'Bearer {os.getenv("LIGHT_APP_TOKEN")}',
"Content-Type": "application/json",
},
json={
"client_agent_accepted_at": "2026-03-01T14:30:00Z",
},
)
```
### 3. Complete prerequisites
Follow [basic enrollment steps 3–9](/guides/enrollment#3-identify-the-location) to collect the customer's address, verify their identity, run a credit check, and add a payment method. You can supply any of these upfront (at account creation or via the API) to reduce what the customer needs to provide themselves.
Once all prerequisites are satisfied the account moves to `enrollment_pending_client_agent`. Monitor progress by polling `GET /v1/app/accounts/{account_uuid}` or listening for webhooks.
### 4. Request plans and accept on the customer's behalf
When the service start date is known and approaching (for example, when PTO is confirmed), request available plans and accept on the customer's behalf.
#### Timing
Call `plans/request` **7 to 30 days before the intended start date**. Priced plans expire after 5 business days if not accepted, so don't call this too far in advance.
Use `POST /v1/app/accounts/{account_uuid}/enroll/plans/request` to retrieve current plans:
```javascript
const { plans } = await fetch(
`https://api.light.dev/v1/app/accounts/${accountUuid}/enroll/plans/request`,
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.LIGHT_APP_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ esi_id: "55512345671234567" }),
},
).then((r) => r.json());
```
```python
data = requests.post(
f"https://api.light.dev/v1/app/accounts/{account_uuid}/enroll/plans/request",
headers={"Authorization": f'Bearer {os.getenv("LIGHT_APP_TOKEN")}'},
json={"esi_id": "55512345671234567"},
).json()
plans = data["plans"]
```
Then accept the chosen plan using `POST /v1/account/enroll/plans/accept` with the account token. Make sure to pass the `esi_id` alongside the `plan_uuid` and `service_start_date`, as it's required to create the service location.
`service_start_date` must be **sometime between 7–30 days from today**. The 7-day minimum gives the customer time to opt out before service begins; the 30-day maximum keeps pricing current.
```javascript
await fetch("https://api.light.dev/v1/account/enroll/plans/accept", {
method: "POST",
headers: {
Authorization: `Bearer ${accountToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
plan_uuid: "d25c31d4-...",
esi_id: "55512345671234567",
service_start_date: "2026-04-01",
terms_accepted: true,
}),
});
```
```python
requests.post(
"https://api.light.dev/v1/account/enroll/plans/accept",
headers={"Authorization": f"Bearer {account_token}"},
json={
"plan_uuid": "d25c31d4-...",
"esi_id": "55512345671234567",
"service_start_date": "2026-04-01",
"terms_accepted": True,
},
)
```
After the plan is accepted, call `POST /v1/account/enroll/finalize` with `is_client_agent_enrollment` set to `true` in the request body. See [basic enrollment step 10](/guides/enrollment#10-finalize-enrollment) for when to call finalize and what happens next. The `enrollment.plan_accepted` webhook fires when the plan is accepted (above). The `enrollment.finalized` webhook fires when finalize succeeds.
```json title="Request"
{
"is_client_agent_enrollment": true
}
```
As part of plan confirmation, a **Letter of Authorization (LOA)** is automatically generated and sent to the customer. This documents that you accepted the plan on their behalf and what they were enrolled in, and is part of fulfilling your transparency obligations to them.
---
## Frozen credit
If the customer's credit is frozen, the account moves to `credit_check_frozen` and the customer receives an email with a link to Experian's unfreeze portal. Once they unfreeze their credit, we automatically retry the check and the account returns to `enrollment_pending_client_agent`. No action needed on your end.
## Related webhooks
Webhooks let your application receive real-time notifications about status changes without polling. [Learn more](/webhooks)
### `enrollment.plan_accepted`
Triggered when a plan is accepted on the customer's behalf.
### `enrollment.finalized`
Triggered when enrollment finalization completes.
### `account.payment_method_added`
Triggered when the customer adds a payment method.
### `location.service_active`
Triggered when electricity service becomes active for the location.
---
# 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 Development > App Token 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.
---
# 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#9-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.
### `account.invoice_sent`
Triggered when a billing statement email is sent to a customer for an invoice. This event is not sent when an invoice email is resent.
---
# 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
`created`
`unresolved`
Customer account has just been created and has no status yet.
`created`
`missing_address`
Customer has not provided an address to enroll.
`applying`
`plan_not_accepted`
Customer has not accepted a plan to enroll.
`applying`
`missing_identity`
Customer has not provided additional information such as a birth date or social security number to enroll.
`applying`
`missing_payment_method`
Customer has not provided a payment method to enroll.
`applying`
`enrollment_not_finalized`
Customer has completed most steps of the enrollment process but has not yet finalized it.
`reviewing`
`verification_pending`
Customer's enrollment is being verified by us.
`reviewing`
`credit_check_pending`
Customer's credit check is being verified by us.
`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_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`
`enrollment_pending_client_agent`
Customer's preapproval (credit check and payment method) is complete. Waiting for the client agent (partner with power of attorney) to select a plan and start date.
`scheduled`
`service_pending`
Customer's service is scheduled to start on a future date.
`activating`
`utility_submitted`
Customer's enrollment has been submitted to their local utility for processing.
`active`
`active`
Customer's service with us is active.
`cancelled`
`enrollment_blocked_verification_hold`
Customer's enrollment was permanently blocked; customer did not resolve credit in time.
`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);
}
```
---
# 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)
---
# 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-flow)
- `billing` - Launches the embedded flow with the user's invoices. [Preview](/guides/billing#embedded-flow)
- `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.
---
# 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.
---
# 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.
---
# 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
- `account.invoice_sent`: Triggered when a billing statement email is sent to a customer for an invoice (not sent on resends)
- `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
- `location.renewal_email_sent`: Triggered when a customer receives a reminder email to renew the plan for their location
- `comparison_invoice.processed`: Triggered when a comparison invoice is done being processed (either successfully or unsuccessfully)
- `usage_profile.completed`: Triggered when a usage profile has been successfully processed
- `usage_profile.failed`: Triggered when a usage profile fails to process
## 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` object, and sometimes a `location` if one is set for the account.
The `account.invoice_sent` event also includes an `invoice_number` string identifying the billing statement that was sent.
The "usage_profile.\*" events will contain a `usage_profile` object with `uuid`, `status`, `utility_account_number`, `start_date`, `end_date`, `error_message`, and `completed_at`.
## 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.
---
# 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#embedded-flows)
## 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.
When your renewal experience is ready, set `renewals_url` on the customer's account with the `PATCH /v1/app/accounts/{account_uuid}` endpoint. We'll use that URL in renewal notices instead of our default hosted flow.
Setting this field replaces our default renewal experience for that account. Enable it only after you've validated your renewal API integration end to end.
### 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.
### Overriding default renewals
When your renewal experience is ready, you can set `renewals_url` on the customer's account with the `PATCH /v1/app/accounts/{account_uuid}` endpoint. We'll use your URL in regulatory renewal notices instead of the link to our default hosted flow.
Setting this field replaces our default renewal experience for that account. Enable it only after you've validated your renewal API integration end to end.
## 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.
### `location.renewal_email_sent`
Triggered when a customer receives a reminder email to renew the plan for their location.
---
# comparison-invoices
> https://docs.light.dev/guides/comparison-invoices
# Plan comparison
## Intro
Comparison invoices and usage profiles let you show customers how your plans compare to their current electricity provider, personalized with their actual usage data. A comparison invoice captures what a customer currently pays, and a usage profile pulls their historical interval usage to produce more accurate rate comparisons.
Comparison invoice endpoints use `AccountToken` [authentication](/authentication), while usage profile endpoints use `AppToken` authentication. Refer to the [authentication docs](/authentication) for more details on each.
Comparison invoices currently work best for customers on simple energy plans. Results are less accurate for customers with solar panels, battery storage, or those in the process of installing solar, since export credits and net metering charges are not yet fully accounted for in the comparison. We plan to enhance this in the future.
## API
### 1. Upload a comparison invoice
The customer uploads a PDF of their current electricity bill. Use `POST /v1/account/comparison-invoice` to upload the invoice. The invoice will be parsed asynchronously in the background.
```javascript
const formData = new FormData();
formData.append("file", fs.createReadStream("invoice.pdf"));
const response = await fetch(
"https://api.light.dev/v1/account/comparison-invoice",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.LIGHT_ACCOUNT_TOKEN}`,
},
body: formData,
},
);
const invoice = await response.json();
console.log("Invoice UUID:", invoice.uuid);
```
```python
import requests
import os
with open("invoice.pdf", "rb") as f:
response = requests.post(
"https://api.light.dev/v1/account/comparison-invoice",
headers={
"Authorization": f'Bearer {os.getenv("LIGHT_ACCOUNT_TOKEN")}',
},
files={"file": ("invoice.pdf", f, "application/pdf")},
)
invoice = response.json()
print(f"Invoice UUID: {invoice['uuid']}")
```
```bash
$ curl -X POST "https://api.light.dev/v1/account/comparison-invoice" \
-H "Authorization: Bearer $LIGHT_ACCOUNT_TOKEN" \
-F "file=@invoice.pdf"
```
```json title="Example response"
{
"uuid": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
"status": "pending",
"utility_account_number": null,
"processed_at": null,
"processing_error": null
}
```
### 2. Wait for invoice processing
After upload, the invoice is processed asynchronously. You can poll the status with `GET /v1/account/comparison-invoice` or listen for the `comparison_invoice.processed` webhook.
We recommend using the `comparison_invoice.processed` webhook instead of polling. Your system will be notified as soon as the invoice is ready. [Learn more about webhooks](/webhooks)
```javascript
const response = await fetch(
"https://api.light.dev/v1/account/comparison-invoice",
{
headers: {
Authorization: `Bearer ${process.env.LIGHT_ACCOUNT_TOKEN}`,
},
},
);
const invoice = await response.json();
console.log("Status:", invoice.processed_at ? "processed" : "pending");
```
```python
response = requests.get(
"https://api.light.dev/v1/account/comparison-invoice",
headers={
"Authorization": f'Bearer {os.getenv("LIGHT_ACCOUNT_TOKEN")}',
},
)
invoice = response.json()
status = "processed" if invoice["processed_at"] else "pending"
print(f"Status: {status}")
```
```bash
$ curl "https://api.light.dev/v1/account/comparison-invoice" \
-H "Authorization: Bearer $LIGHT_ACCOUNT_TOKEN"
```
```json title="Example response"
{
"uuid": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
"status": "processed",
"utility_account_number": "10443720000000000",
"processed_at": "2025-06-15T14:32:00Z",
"processing_error": null
}
```
If the `processing_error` field is set, the invoice could not be parsed. Check the error message for details and consider re-uploading a clearer copy of the invoice.
### 3. Run a simple comparison
Once the invoice is processed, compare the customer's current bill against one of your [plan groups](/key-concepts#plan-groups) using `GET /v1/account/comparison-invoice/simple-compare`.
At this stage, the comparison uses the usage data extracted from the invoice itself. This provides a baseline comparison showing what the customer would have paid on your plan.
```javascript
const params = new URLSearchParams({
plan_group: "PLAN_GROUP_UUID",
uuid: invoice.uuid,
});
const response = await fetch(
`https://api.light.dev/v1/account/comparison-invoice/simple-compare?${params}`,
{
headers: {
Authorization: `Bearer ${process.env.LIGHT_ACCOUNT_TOKEN}`,
},
},
);
const comparison = await response.json();
console.log("Invoice total:", comparison.parsed_invoice_data.total_dollars);
console.log("Plan total:", comparison.plan_comparison.total_dollars);
```
```python
response = requests.get(
"https://api.light.dev/v1/account/comparison-invoice/simple-compare",
headers={
"Authorization": f'Bearer {os.getenv("LIGHT_ACCOUNT_TOKEN")}',
},
params={
"plan_group": "PLAN_GROUP_UUID",
"uuid": invoice["uuid"],
},
)
comparison = response.json()
print(f"Invoice total: {comparison['parsed_invoice_data']['total_dollars']}")
print(f"Plan total: {comparison['plan_comparison']['total_dollars']}")
```
```bash
$ curl "https://api.light.dev/v1/account/comparison-invoice/simple-compare?plan_group=PLAN_GROUP_UUID&uuid=INVOICE_UUID" \
-H "Authorization: Bearer $LIGHT_ACCOUNT_TOKEN"
```
```json title="Example response"
{
"processing_error": null,
"parsed_invoice_data": {
"contract_end_date": "2024-01-01",
"invoice_date": "2023-06-15",
"provider_display_name": "TXU Energy",
"total_dollars": "187.43",
"total_kwh": "1523.0"
},
"plan_comparison": {
"group_uuid": "4e7acc79-58af-4be1-881f-c9ccbbe02a80",
"name": "Clean Energy",
"total_dollars": "162.18",
"usage_profile_uuid": null
}
}
```
See the API reference for the complete response format.
### 4. Run an annual comparison
For a full-year savings projection, use `GET /v1/account/comparison-invoice/annual-compare`. This projects costs across all 12 calendar months, comparing what the customer would pay on their current plan versus your plan over a typical year.
```javascript
const params = new URLSearchParams({
plan_group: "PLAN_GROUP_UUID",
uuid: invoice.uuid,
});
const response = await fetch(
`https://api.light.dev/v1/account/comparison-invoice/annual-compare?${params}`,
{
headers: {
Authorization: `Bearer ${process.env.LIGHT_ACCOUNT_TOKEN}`,
},
},
);
const comparison = await response.json();
console.log(
"Current plan annual cost:",
comparison.annual_totals.annual_current_plan_dollars,
);
console.log(
"Offered plan annual cost:",
comparison.annual_totals.annual_offered_plan_dollars,
);
```
```python
response = requests.get(
"https://api.light.dev/v1/account/comparison-invoice/annual-compare",
headers={
"Authorization": f'Bearer {os.getenv("LIGHT_ACCOUNT_TOKEN")}',
},
params={
"plan_group": "PLAN_GROUP_UUID",
"uuid": invoice["uuid"],
},
)
comparison = response.json()
print(f"Current plan annual cost: {comparison['annual_totals']['annual_current_plan_dollars']}")
print(f"Offered plan annual cost: {comparison['annual_totals']['annual_offered_plan_dollars']}")
```
```bash
$ curl "https://api.light.dev/v1/account/comparison-invoice/annual-compare?plan_group=PLAN_GROUP_UUID&uuid=INVOICE_UUID" \
-H "Authorization: Bearer $LIGHT_ACCOUNT_TOKEN"
```
```json title="Example response (months array truncated to 2 of 12)"
{
"processing_error": null,
"months": [
{
"month": 1,
"import_kwh": "950.00",
"current_plan_dollars": "120.50",
"offered_plan_dollars": "96.40"
},
{
"month": 2,
"import_kwh": "875.00",
"current_plan_dollars": "111.00",
"offered_plan_dollars": "88.80"
}
],
"annual_totals": {
"annual_import_kwh": "14000.00",
"annual_current_plan_dollars": "1774.50",
"annual_offered_plan_dollars": "1419.60"
},
"parsed_invoice_data": {
"contract_end_date": "2024-01-01",
"invoice_date": "2023-06-15",
"provider_display_name": "TXU Energy",
"total_dollars": "187.43",
"total_kwh": "1523.0"
},
"plan_comparison": {
"group_uuid": "4e7acc79-58af-4be1-881f-c9ccbbe02a80",
"name": "Clean Energy",
"total_dollars": "1419.60",
"usage_profile_uuid": null
}
}
```
The `months` array always contains 12 entries (January through December), each with projected usage and costs on both plans. See the API reference for the complete response format.
#### How to interpret the results
The simulated cost for the invoice month will be close to the uploaded bill, but not identical. Annual compare is a typical-year projection, not a replay of one billing period. Billing periods also rarely align perfectly with calendar month boundaries. Use simple compare if you need an exact match to the invoice amount.
**Think of annual compare as "what should I expect to pay over the next year."** A single invoice captures one month, which may be unusually high or low depending on the season. Annual compare accounts for the full range of a customer's usage across all 12 months, giving them a realistic picture of what switching plans would actually mean for their annual bill.
It's especially revealing when a plan's structure varies by usage level across seasons. Consider a plan with a high energy rate but a $125 credit that only applies above 1,000 kWh. A summer bill at peak usage might show only $3 in savings with simple compare. But simulate the full year and many months fall below the threshold, meaning no credit applies, and the customer would actually save closer to $500 annually. The same applies to plans with free nights, weekends, or tiered rates that kick in at different usage bands.
| | Simple compare | Annual compare |
| --------------------------------------------- | --------------------------- | -------------------------------- |
| **Best for** | Matching a specific invoice | Projecting a full year |
| **Usage basis** | Exact invoice kWh | SMT data or simulated load curve |
| **Handles tiered credits, free nights, etc.** | Partially | Yes, across all months |
| **Invoice month matches exactly** | Yes | No (by design) |
If you need the exact uploaded bill amount to appear alongside the annual projection, you can substitute the simple compare result for the invoice month while using annual compare for the remaining 11 months. Although using the annual data as-is will be the most accurate estimate.
## Enhancing comparisons with usage profiles
Without a usage profile, the rates offered to a customer are based on an average of all customers on your plan. This means some customers pay more than their actual cost to serve, and others pay less. Usage profiles let you offer personalized rates based on a customer's real energy consumption patterns. Customers who are lower cost to serve (for example, those with efficient homes or favorable usage patterns) will receive lower rates, while higher cost customers will receive rates that reflect their actual usage.
This creates more accurate and fair pricing, and lets you reward customers for better energy habits. If the customer authorizes access to their utility meter data, you can fetch their actual 15-minute interval usage history to build a usage profile.
Usage profiles currently do not support customers with solar panels, battery storage, or those in the process of installing solar. We will likely enhance this in the future, so feel free to reach out to our team if you are interested in these features to help gauge interest.
Once a usage profile has been successfully fetched for a customer, all subsequent rates for that utility account number will use the personalized rates. You cannot fall back to average rates for that customer. This prevents selectively offering personalized rates only when they are cheaper, which would skew the average cost over time.
### Fetch a usage profile
Use `POST /v1/app/usage-profiles/fetch` to trigger a data fetch from Smart Meter Texas (SMT), building a usage profile spanning the customer's last 12+ months of electricity consumption. This endpoint uses `AppToken` authentication (not `AccountToken`).
You must obtain explicit authorization from the customer before fetching their usage data. In Texas, this authorizes access to their Smart Meter Texas data. Set `user_authorizes_usage_fetch` to `true` only after the customer has consented.
```javascript
const response = await fetch(
"https://api.light.dev/v1/app/usage-profiles/fetch",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.LIGHT_APP_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
comparison_invoice_uuid: invoice.uuid,
user_authorizes_usage_fetch: true,
}),
},
);
const usageProfile = await response.json();
console.log("Usage profile UUID:", usageProfile.uuid);
console.log("Status:", usageProfile.status);
```
```python
response = requests.post(
"https://api.light.dev/v1/app/usage-profiles/fetch",
headers={
"Authorization": f'Bearer {os.getenv("LIGHT_APP_TOKEN")}',
"Content-Type": "application/json",
},
json={
"comparison_invoice_uuid": invoice["uuid"],
"user_authorizes_usage_fetch": True,
},
)
usage_profile = response.json()
print(f"Usage profile UUID: {usage_profile['uuid']}")
print(f"Status: {usage_profile['status']}")
```
```bash
$ curl -X POST "https://api.light.dev/v1/app/usage-profiles/fetch" \
-H "Authorization: Bearer $LIGHT_APP_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"comparison_invoice_uuid": "INVOICE_UUID",
"user_authorizes_usage_fetch": true
}'
```
```json title="Example response (202 Accepted)"
{
"uuid": "b2c3d4e5-6789-01ab-cdef-234567890abc",
"status": "pending",
"utility_account_number": "10443720000000000",
"error_message": null,
"created_at": "2025-06-15T14:35:00Z",
"start_date": null,
"end_date": null
}
```
Sandbox apps use synthetic usage data instead of real SMT data. This lets you test the full flow without requiring a real utility account.
### Check usage profile status
The usage profile is processed asynchronously. Poll the status with `GET /v1/app/usage-profiles/{usage_profile_uuid}` or use webhooks to be notified when processing completes.
We recommend using the `usage_profile.completed` and `usage_profile.failed` webhooks instead of polling. [Learn more about webhooks](/webhooks)
```javascript
const response = await fetch(
`https://api.light.dev/v1/app/usage-profiles/${usageProfile.uuid}`,
{
headers: {
Authorization: `Bearer ${process.env.LIGHT_APP_TOKEN}`,
},
},
);
const profile = await response.json();
console.log("Status:", profile.status);
```
```python
response = requests.get(
f"https://api.light.dev/v1/app/usage-profiles/{usage_profile['uuid']}",
headers={
"Authorization": f'Bearer {os.getenv("LIGHT_APP_TOKEN")}',
},
)
profile = response.json()
print(f"Status: {profile['status']}")
```
```bash
$ curl "https://api.light.dev/v1/app/usage-profiles/USAGE_PROFILE_UUID" \
-H "Authorization: Bearer $LIGHT_APP_TOKEN"
```
```json title="Example response"
{
"uuid": "b2c3d4e5-6789-01ab-cdef-234567890abc",
"status": "completed",
"utility_account_number": "10443720000000000",
"error_message": null,
"created_at": "2025-06-15T14:35:00Z",
"start_date": "2024-06-01",
"end_date": "2025-06-14"
}
```
The `status` field will be one of:
- `pending`: The usage profile is still being processed
- `completed`: The usage profile is ready and will be applied to plan comparisons
- `failed`: Processing failed. Check the `error_message` field for details.
### How usage profiles affect comparisons and rates
You don't need to re-run anything after a usage profile completes. Once a usage profile is available, it is automatically applied going forward. Any subsequent calls to the simple compare endpoint, the annual compare endpoint, or the enrollment plans endpoint for this utility account number will use the customer's actual interval usage data to calculate customized energy rates.
You can tell that a usage profile is being used by checking the `usage_profile_uuid` field in the response. When it is set, the rates reflect the customer's real usage pattern rather than generalized assumptions.
```json title="Example simple compare response with usage profile"
{
"processing_error": null,
"parsed_invoice_data": {
"contract_end_date": "2024-01-01",
"invoice_date": "2023-06-15",
"provider_display_name": "TXU Energy",
"total_dollars": "187.43",
"total_kwh": "1523.0"
},
"plan_comparison": {
"group_uuid": "4e7acc79-58af-4be1-881f-c9ccbbe02a80",
"name": "Clean Energy",
"total_dollars": "158.92",
"usage_profile_uuid": "b2c3d4e5-6789-01ab-cdef-234567890abc"
}
}
```
## 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)
### `comparison_invoice.processed`
Triggered when a comparison invoice is done being processed (either successfully or unsuccessfully). Check the invoice status to determine if processing was successful.
### `usage_profile.completed`
Triggered when a usage profile has been successfully processed and is ready to use for plan comparisons.
### `usage_profile.failed`
Triggered when a usage profile fails to process. The webhook payload includes an `error_message` field with details about the failure.
---