> ## Documentation Index
> Fetch the complete documentation index at: https://docs.incentives.leap.energy/llms.txt
> Use this file to discover all available pages before exploring further.

# Incentives API Guide

> Guide to using the Incentives API for calculating rebates and creating applications

## Overview

The Incentives API calculates incentives across multiple utility programs for a customer address and device. It automatically handles customer creation, utility lookup, and incentive aggregation.

For detailed endpoint specifications, see the [Check Incentives](/api-reference/endpoint/check-incentives) API reference.

## Operation Types

The Incentives API supports three operation modes:

### `lookup` Operation

Use for first-time customers or when you need fresh utility data:

* **Required fields**: `address`, `building_type`, and `device_ids` or `device_names`
* Creates a new customer if not found
* Geocodes the address to find applicable utilities
* Caches utility data on the customer record for future use
* Creates `customer_device` records for all devices (including duplicates when using `device_ids`)
* Slower due to address processing and utility lookup
* **Device handling**: On first call, creates all devices as customer devices. On subsequent calls, validates that all devices already exist for the customer.

### `refresh` Operation

Use for returning customers with cached utility data (faster):

* **Required fields**: Only `reference_id`
* Customer must already exist
* **Optional fields**:
  * `address`: If provided and differs from stored address, updates customer address (still uses cached utilities, no geocoding)
  * `building_type`: If not provided, resolved from customer's stored `building_type_id` in the database
  * `device_ids`:
    * **If not provided**: Uses all existing devices from customer's `customer_devices` table
    * **If provided**: Validates that all `device_ids` already exist for the customer, then uses those devices
* Significantly faster—no geocoding or utility lookup required
* Uses cached utility data from previous lookup
* **Device handling**: Without `device_ids`, uses all existing devices. With `device_ids`, validates they exist before using them.

<Note>
  `device_names` is **not supported** for refresh operations. Use `device_ids` to specify devices, or omit devices entirely to use all existing devices.
</Note>

### `override` Operation

Use when a customer changes their devices completely:

* **Required fields**: `reference_id`, `building_type`, and `device_ids` or `device_names`
* Customer must already exist
* **Optional fields**:
  * `address`: If provided and differs from stored address, updates customer address (still uses cached utilities, no geocoding)
* **Device handling**: Deletes ALL existing customer devices, then creates new ones from the provided devices
* Then refreshes calculation using cached utilities (faster than lookup)
* Use when you need to completely replace a customer's device list without validation errors

<Tip>
  **Performance Tip**: Always use `operation_type="refresh"` for returning customers with the same devices. This skips geocoding and uses cached utility data, providing much faster responses. Use `override` only when devices have changed.
</Tip>

## Reference IDs

The `reference_id` field is **required** and serves multiple purposes:

* Must be unique within your account
* Enables fast lookups on subsequent API calls
* Use your internal customer/account ID for easy correlation

<Note>
  Reference IDs are scoped to your account. The same `reference_id` can exist in different customer accounts without conflict.
</Note>

## Creating Applications

Set `create_application: true` in the Incentives request to automatically create application records. When the customer is eligible for at least one program and `create_application` is true, the response includes a `connect_url` field with a link to the customer application portal.

This streamlines the workflow by combining incentive calculation and application creation in a single API call.

<Note>
  `connect_url` is returned as an empty string when `total_incentive_amount` is `0` or when `create_application` was not requested. Check for a non-empty value before presenting the link to a customer.
</Note>

## Processing Status

The response separates two ideas: what a customer is *eligible* for, and what Leap is currently able to *process an application for*.

Each entry in `by_program` includes a `processing_enabled` boolean. It is `true` when the program's form requirements have been fully captured in Leap's system, meaning we can submit an application on the customer's behalf today. It is `false` when the program is still being onboarded — eligibility and incentive amounts are still calculated, but applications cannot yet be completed.

The top-level `processing_summary` object summarizes the processing-enabled subset:

```json theme={null}
{
  "processing_summary": {
    "program_ids": [838, 1001],
    "incentive_amount": 1250
  }
}
```

* `program_ids` — IDs of the programs from `by_program` whose `processing_enabled` is `true`.
* `incentive_amount` — sum of `total_incentive` across those programs, in USD.

### How to use it

* **Customer-facing offer amount**: use `processing_summary.incentive_amount`. This is what Leap can actually process today.
* **Forecasting / pipeline reporting**: use `incentives.total_incentive_amount`. This includes programs still being onboarded.
* **Hiding offers when nothing is processable**: when `processing_summary.program_ids` is empty, the `connect_url` will also be empty. Show a "coming soon" state rather than the Connect link.

## Device IDs

The `device_ids` field is an **array** that can contain multiple device IDs, including duplicates:

* **Array format**: `device_ids: [42, 44, 44, 44]` creates 4 customer device records (1 for device 42, 3 for device 44)
* **Device categories**: The API extracts unique device categories from all `device_ids` and looks up programs for all device types (e.g., EV Chargers, Thermostats, Heat Pumps)
* **Required for**: `lookup` and `override` operations
* **Optional for**: `refresh` operation (uses existing devices if not provided)

<Note>
  When using `refresh` without `device_ids`, the API uses all existing devices from the customer's `customer_devices` table. This allows refreshing with just the `reference_id`.
</Note>

## Device Names

As an alternative to `device_ids`, you can pass `device_names` — an array of your own equipment or product name strings. The API resolves these to Leap device IDs using your organization's **equipment map**.

* **Supported for**: `lookup` and `override` operations
* **Not supported for**: `refresh` operations
* **Mutually exclusive** with `device_ids` — providing both returns a `400` error

### Resolution Behavior

The API matches each name against your organization's equipment map and resolves it to a Leap device ID:

* **All names resolve**: The request proceeds normally using the resolved device IDs.
* **Partial resolution**: The request proceeds with the resolved devices. The response includes a `warnings` object listing the unresolved names so you can update your equipment map.
* **Zero resolution**: The request fails with a `400` error and an `unresolved_device_names` array listing the names that could not be matched.

<Tip>
  When the response includes a `warnings.unresolved_device_names` array, review the listed names and work with Leap to update your equipment map. Future requests with those names will then resolve automatically.
</Tip>

### Example Response with Warnings

When some device names cannot be resolved, the response includes a `warnings` object alongside the normal incentive results. All standard response fields (`processing_summary`, `connect_url`, etc.) are still present:

```json theme={null}
{
  "utility": { "eiaid": "14354", "name": "PacifiCorp (Oregon)" },
  "possible_utilities": [{ "eiaid": "14354", "name": "PacifiCorp (Oregon)" }],
  "address": "456 Oak Ave, Portland, OR, 97201",
  "building_type": "RESIDENTIAL",
  "customer_id": 790,
  "programs_evaluated": 2,
  "incentives": {
    "total_incentive_amount": 1500,
    "total_eligible_combinations": 1,
    "by_program": [...]
  },
  "processing_summary": {
    "program_ids": [838],
    "incentive_amount": 1500
  },
  "connect_url": "https://connect.incentives.leap.energy/your-company/refId/external-ref-456",
  "reference_id": "external-ref-456",
  "warnings": {
    "unresolved_device_names": ["Unknown Model XYZ"],
    "message": "1 device name(s) could not be resolved and were skipped."
  }
}
```

## Example Workflow

### First-Time Customer (Lookup)

```bash theme={null}
curl -X POST "https://api.incentives.leap.energy/alpha/incentives" \
  -H "x-api-key: leap_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "operation_type": "lookup",
    "reference_id": "customer-123",
    "address": {
      "street_1": "123 Main St",
      "city": "San Francisco",
      "state_or_province_code": "CA",
      "postal_code": "94102",
      "country_code": "US"
    },
    "building_type": "RESIDENTIAL",
    "device_ids": [42],
    "create_application": true
  }'
```

### First-Time Customer (Lookup with Device Names)

```bash theme={null}
curl -X POST "https://api.incentives.leap.energy/alpha/incentives" \
  -H "x-api-key: leap_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "operation_type": "lookup",
    "reference_id": "customer-456",
    "address": {
      "street_1": "456 Oak Ave",
      "city": "Portland",
      "state_or_province_code": "OR",
      "postal_code": "97201",
      "country_code": "US"
    },
    "building_type": "RESIDENTIAL",
    "device_names": ["Mitsubishi Hyper-Heat MSZ-RXT09", "Rheem ProTerraDERA2060"],
    "create_application": true
  }'
```

### Returning Customer (Refresh - Minimal)

```bash theme={null}
curl -X POST "https://api.incentives.leap.energy/alpha/incentives" \
  -H "x-api-key: leap_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "operation_type": "refresh",
    "reference_id": "customer-123",
    "create_application": true
  }'
```

### Returning Customer (Refresh - With Device Validation)

```bash theme={null}
curl -X POST "https://api.incentives.leap.energy/alpha/incentives" \
  -H "x-api-key: leap_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "operation_type": "refresh",
    "reference_id": "customer-123",
    "device_ids": [42, 44],
    "create_application": true
  }'
```

### Customer Changed Devices (Override)

```bash theme={null}
curl -X POST "https://api.incentives.leap.energy/alpha/incentives" \
  -H "x-api-key: leap_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "operation_type": "override",
    "reference_id": "customer-123",
    "building_type": "RESIDENTIAL",
    "device_ids": [55, 66, 77],
    "create_application": true
  }'
```

## Best Practices

### Choose the Right Operation Type

* **First interaction**: Use `lookup` with full address and `device_ids` or `device_names`
* **Subsequent calls (same devices)**: Use `refresh` with just the `reference_id` (fastest option)
* **Subsequent calls (validate specific devices)**: Use `refresh` with `reference_id` and `device_ids`
* **Customer changed devices**: Use `override` to replace all devices
* **Address changes**: Use `lookup` to update utility information (geocodes new address)

### Device ID Strategy

* Use arrays for `device_ids` even for single devices: `[42]` not `42`
* Include duplicates in the array if customer has multiple of the same device: `[42, 42, 42]`
* For `refresh` operations, you can omit `device_ids` to use all existing devices
* For `override` operations, provide the complete new device list (old devices are deleted)

### Device Name Strategy

* Use `device_names` when you have equipment model names or product strings but not Leap device IDs
* Check the `warnings` object in the response to catch unresolved names early
* `device_names` works with `lookup` and `override` operations only — use `device_ids` for `refresh`
* You cannot combine `device_ids` and `device_names` in the same request

### Reference ID Strategy

* Use your internal customer/account ID as the `reference_id`
* Maintain consistency across all API calls for the same customer
* Store the mapping between your IDs and Leap's `customer_id` for reference
* Reference IDs are scoped to your organization (same ID can exist in different organizations)

## Error Responses

Non-`2xx` responses follow a consistent shape:

```json theme={null}
{
  "error": "No utility found for this address",
  "error_code": "no_utility_at_address",
  "details": {
    "upstream": "utility-programs-lookup",
    "upstream_status": 404
  }
}
```

* **`error`** — human-readable message, suitable for logs or support tickets. May be revised over time.
* **`error_code`** — stable, typed identifier. Build retry logic and branching against `error_code`, not against the message.
* **`details`** — optional diagnostic payload whose shape varies by `error_code`.

### Error code reference

| `error_code`                             | Status | When it happens                                                   | Retry?                                                                                     |
| ---------------------------------------- | ------ | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `no_utility_at_address`                  | 404    | Address is not covered by any known utility in our territory data | **No** — address is not serviceable; contact Leap if you believe this is a gap in our data |
| `address_not_geocodable`                 | 422    | Address could not be resolved by the geocoder                     | **No** — correct the address and resubmit                                                  |
| `no_valid_eiaid`                         | 404    | Matched utilities are missing EIA IDs in our catalog              | **No** — contact Leap                                                                      |
| `invalid_building_type`                  | 400    | `building_type` is not a supported value                          | **No** — use `RESIDENTIAL`, `MULTIFAMILY`, `MANUFACTURED_HOME`, or `COMMERCIAL`            |
| `building_type_unresolved`               | 400    | Could not resolve building type during a `refresh`                | **No** — include `building_type` explicitly                                                |
| `invalid_json_body`                      | 400    | Request body was not valid JSON                                   | **No**                                                                                     |
| `mutually_exclusive_device_inputs`       | 400    | Request included both `device_ids` and `device_names`             | **No** — send one or the other                                                             |
| `device_names_not_supported_for_refresh` | 400    | `device_names` is not supported for `refresh`                     | **No** — use `device_ids` or omit devices                                                  |
| `missing_devices`                        | 400    | `device_ids` / `device_names` required for lookup or override     | **No**                                                                                     |
| `no_device_categories`                   | 400    | Provided `device_ids` don't exist in our catalog                  | **No**                                                                                     |
| `upstream_transient`                     | 502    | Transient upstream failure (geocoder, programs service)           | **Yes** — exponential backoff                                                              |
| `upstream_infra`                         | 502    | Upstream infrastructure error                                     | **Yes** — exponential backoff                                                              |
| `device_name_resolution_failed`          | 500    | Internal error resolving device names                             | **Yes** — exponential backoff                                                              |
| `delete_customer_devices_failed`         | 500    | Internal error during `override`                                  | **Yes** — exponential backoff                                                              |
| `cached_utilities_read_failed`           | 500    | Internal error reading cached utilities                           | **Yes** — exponential backoff                                                              |
| `internal_error`                         | 500    | Unexpected server error                                           | **Yes** — exponential backoff                                                              |

<Tip>
  **Rule of thumb:** retry on `5xx`, don't retry on `4xx`. The `details.upstream_*` fields on `5xx` responses help Leap support diagnose upstream issues — include them if you file a ticket.
</Tip>

<Note>
  A small number of validation errors (e.g. reference ID mismatches, missing required fields on new-customer creation) currently return `4xx` responses without an `error_code` field. When `error_code` is absent, fall back to matching on the `error` message. These paths are being migrated to the typed system.
</Note>

### Retry strategy

For `5xx` errors, use exponential backoff — start at 1 second, double up to a cap of \~30 seconds, with a total retry budget around 60 seconds. Add random jitter to avoid thundering herd.

Do not retry `4xx` responses. They indicate a request-level problem that won't resolve without a change on your end (or a data fix on ours).

### Common scenarios

* **No utility found (`no_utility_at_address`, 404)**: the address does not match any utility service territory in our data. This is not a transient failure — retrying the same address will return the same result. If you believe the address should be serviced, report it to Leap with the `details.upstream_body` fields so we can investigate the territory polygon.
* **Reference ID mismatch (400, no `error_code` yet)**: the customer already exists in your account under a different `reference_id`. Use the original `reference_id` for this customer, or use a new one for a new customer.
* **Device not found on refresh**: when using `refresh` with `device_ids`, all IDs must already be associated with the customer. Use `override` if you need to replace the device list.
* **Unresolved device names**: if zero names resolve, the request returns `400` with an `unresolved_device_names` array. If some resolve, the request succeeds and the response includes `warnings.unresolved_device_names` — update your equipment map for future calls.

## Related Resources

* [Check Incentives API Reference](/api-reference/endpoint/check-incentives)
* [Applications API Guide](/api-reference/guides/api-guide#applications-api)
* [API Authentication](/api-reference/guides/api-guide#authentication--security)
