Home / Developer API / Integration guide
API integration guide
For the engineering team connecting the Incentive Navigator API to your product: how to authenticate, the four calls that take a street address to a calculated incentive amount, and how errors and limits behave. Every request and response shown here was checked against the production API in September 2026.
01 At a glance
One base URL, JSON over HTTPS
- Base URL
- https://api.incentifind.com
- Reference
- /v1/reference
- OpenAPI document
- /v1/openapi.json
- Authentication
- API key in a request header
- Field names
- snake_case
The interactive reference and the OpenAPI document are public — no key needed — and the OpenAPI document is the authoritative list of endpoints: apart from the reference and the document themselves, a /v1 path it does not list returns 404.
A note on names: the API is served from api.incentifind.com, and its reference page and OpenAPI document still carry the IncentiFind name. They describe the same API as this guide.
02 Authentication
Keys are issued by our team
There is no self-serve sign-up and no separate test or sandbox key. To request a key, use the contact form with the topic API access, and tell us what you are building and which of the calls below you need — each key is scoped to specific endpoints.
A key begins with icf_live_. Send it on every request, in either header:
X-API-Key: icf_live_…
Authorization: Bearer icf_live_…
A request with no key, or with a key we do not recognise, is refused with 401 unauthorized. Keep the key on your servers; do not ship it to a browser or mobile app.
03 Address to amount
Four calls, from a street address to a dollar figure
The examples follow one multifamily property in Houston, Texas, and one CenterPoint Energy program that applies to it. Treat the figures as illustrative: programs and their rules are revised as we update the catalog, so the same request can return different values later.
Step 1 · POST /v1/properties/incentives
Find the programs that apply at an address
curl -X POST 'https://api.incentifind.com/v1/properties/incentives' \
-H "X-API-Key: $API_KEY" \
-H 'Content-Type: application/json' \
-d '{
"address": "8000 W Tidwell Rd, Houston, TX 77040",
"asset_type": "commercial_multi_family"
}'
asset_type is required because matching is scoped to the kind of building: at this address, commercial_multi_family returns six programs we can calculate and commercial_office returns four, not all of them the same. Leave it out and the response is 422 validation_error, listing the accepted values. The value you send is echoed back in data.property.asset_type.
Response (excerpt)
{
"data": {
"property": {
"formatted_address": "8000 W Tidwell Rd, Houston, TX 77040",
"county": "Harris",
"state": "TX",
"asset_type": "commercial_multi_family",
…
},
"incentives": [
…
{
"incentive_program_id": 219615,
"admin_name": "CenterPoint Energy",
"calculation_rules": {
"has_approved": true,
"interaction_mode": "calculator",
"requires_measure_selection": true,
…
},
…
},
…
],
"summary": { "total": 157, "computable_programs": 6, … }
},
"meta": { "page": 1, "per_page": 50, "total": 157, "total_pages": 4 }
}
- Pagination. Results come 50 to a page by default. Pass
?page=and?per_page=on the URL, and stop atmeta.total_pages. - Most programs do not produce a number. Here 6 of the 157 can be priced.
summary.computable_programscounts them, and adding"filters": { "computable_only": true }to the request body returns only those. interaction_modetells you what a program needs before you ask for a number:
| Value | Meaning |
|---|---|
calculator | Collect project inputs, then calculate. |
menu | Present the priced options; the user chooses before you calculate. |
source_only | We hold an approved rule set, but none of its rates computes an amount. Show the program and its source. |
null | No approved rule set is in effect for this program. Show the program, not a number. |
Step 2 · GET /v1/incentives/{id} · optional
Read the program record
Returns the program’s descriptive record — name, administrator, dates, eligibility text, website_url — with the same calculation_rules summary nested under data.calculation_rules.
Step 3 · GET /v1/incentives/{id}/calculation-rules
Read how the amount is calculated
curl 'https://api.incentifind.com/v1/incentives/219615/calculation-rules' \
-H "X-API-Key: $API_KEY"
Response (excerpt)
{
"data": {
"incentive_program_id": 219615,
"rule_set_id": "irs_4e944bbe86",
"current_rule_set_id": "irs_4e944bbe86",
"superseded": false,
"source": {
"document_url": "https://visionelements.programprocessing.com/framework/CenterPointTX/2026_CSOP_Program_Manual_Final.pdf",
"document_sha256": "37b323350f0ef6eeff217e03051a8da04cf5302f99be5d3d096e2f25c851e647",
…
},
"interaction_mode": "calculator",
"requires_measure_selection": true,
"selection_groups": [
{ "group_id": "centerpoint_csop_measures", "kind": "pick_one", "option_count": 13, … }
],
"measures": [
…
{
"measure_code": "lighting_led",
"group_kind": "pick_one",
"alternative_group_id": "centerpoint_csop_measures",
"rates": [
{
"kind": "per_unit",
"parameters": { "unit": "kW", "amount_micros": 207000000 },
"required_inputs": ["kilowatts", "is_small_commercial_site", "kwh_saved"],
"unit_label": "$207.00/kW",
…
},
…
]
},
…
],
"disclosures": [ … ],
…
}
}
- Rates are in micros — millionths of a dollar.
207000000is $207.00 per kW; this measure’s energy rate,60000, is $0.06 per kWh. required_inputsnames the inputs a rate needs. Collect them for the measures you will price.- Alternatives. Measures with
group_kind: "pick_one"that share analternative_group_idare alternatives, and a calculation needs one of them chosen.selection_groupslists every such group. - Sources are cited throughout.
sourcenames the rule set’s document by URL and SHA-256 hash; each entry indisclosures— program conditions and terms — carries its own citation, and so does each line of a calculation. Show disclosures alongside any amount.
Step 4 · POST /v1/incentives/{id}/calculate
Calculate the amount
curl -X POST 'https://api.incentifind.com/v1/incentives/219615/calculate?rule_set_id=irs_4e944bbe86' \
-H "X-API-Key: $API_KEY" \
-H 'Content-Type: application/json' \
-d '{
"inputs": { "kilowatts": 20, "kwh_saved": 120000, "is_small_commercial_site": false },
"measure_selections": [
{ "group_id": "centerpoint_csop_measures", "measure_code": "lighting_led" }
]
}'
Response (excerpt)
{
"data": {
"incentive_program_id": 219615,
"rule_set_id": "irs_4e944bbe86",
"calculation_status": "calculated",
"estimate": { "amount_cents": 1134000 },
"breakdown": [
…
{
"measure_code": "lighting_led",
"status": "applied",
"contribution_cents": 414000,
"formula_display": "$207 × kilowatts",
"citation": {
"document_url": "https://visionelements.programprocessing.com/framework/CenterPointTX/2026_CSOP_Program_Manual_Final.pdf",
"document_sha256": "37b323350f0ef6eeff217e03051a8da04cf5302f99be5d3d096e2f25c851e647",
…
},
…
},
{
"measure_code": "lighting_led",
"status": "applied",
"contribution_cents": 720000,
"formula_display": "$0.06 × kwh_saved",
…
},
…
],
"disclosures": [ … ],
…
}
}
$11,340.00: 20 kW at $207 plus 120,000 kWh at $0.06. Calculated amounts are integer cents, and estimate.amount_cents is the figure to use. The breakdown lines show how it was built, but they will not always add up to it: a program cap is applied after the lines are summed, and each line and the total are rounded to cents separately. When a cap reduces the total, limitations says so. For example, 6,000 kW on program 218566 returns one line of 19500000 cents but an estimate.amount_cents of 16250000, with a limitation reading “This estimate has been reduced to the program’s stated maximum of $162,500.” formula_display is text for people, not something to parse. With the rule set pinned (below), the identical request returns the same amounts. Freshness metadata can still change: if we detect a change in the program's source, the same rule set is served with rules.stale set to true.
Branch on calculation_status, not the HTTP status. Each of these is returned with HTTP 200:
| Status | What it means for you |
|---|---|
calculated | A complete amount is in estimate.amount_cents. |
requires_selection | Choose a measure first. required_selections lists each group and its options; estimate is null. |
requires_inputs | Inputs are missing. blocking_missing_inputs names them. |
not_eligible | The project fails a program condition; eligibility_failures says which. Send 10 kW and 50,000 kWh to this program and you get this status: it has a minimum project size of 15 kW or 100,000 kWh. |
not_calculable | We hold no rules that compute an amount for this program; not_calculable_reason says why. |
The reference lists one more status, partial: some rates computed and others could not, so treat the amount as incomplete.
Pin the rule set. Passing the rule_set_id you read in step 3 as ?rule_set_id= prices against exactly that rule set. If a later approval has replaced it, the call returns 409 conflict with the current one in error.details.current_rule_set_id — read the rules again, then recalculate.
04 Errors
One error shape on every /v1 call
{
"error": {
"code": "unauthorized",
"message": "Missing API key",
"details": null
},
"request_id": "req_kdzu4SfJJsnTWJNizjEWKQ"
}
Branch on error.code; message is written for people. details carries specifics when there are any — the offending fields on a 422, the current rule set on a 409. Every response carries an x-request-id header, and on errors it matches request_id. Send your own X-Request-ID header and the API uses it instead. Include it when you report a problem.
| HTTP | error.code | When |
|---|---|---|
| 400 | bad_request | The request body is not valid JSON. |
| 401 | unauthorized | No key, or a key we do not recognise. |
| 403 | forbidden | Listed in the reference: the key is valid but not scoped to this endpoint. |
| 404 | resource_not_found | No program with that ID — or a path or method that does not exist. /v1/properties/incentives accepts only POST, so a GET there returns 404. |
| 409 | conflict | The rule_set_id you pinned has been superseded. |
| 422 | validation_error | The request failed validation; details names each field and the problem. |
| 429 | rate_limit_exceededquota_exceeded | One of the two limits below. |
05 Limits
Two separate limits
Throughput
Responses from the endpoints above carry x-ratelimit-limit (100), x-ratelimit-remaining and x-ratelimit-reset (seconds until the window refills). A body that is not valid JSON (400), a path that does not exist (404) and a throttled request (429 rate_limit_exceeded) come back without them. The window is 60 seconds and is counted per calling IP address and per endpoint, not per key: requests from the same IP share it, including requests with no valid key. Over the limit, the reference documents 429 rate_limit_exceeded with a Retry-After header giving the seconds to wait before retrying.
Call quota
For keys issued with a call cap, the reference documents X-API-Call-Limit, X-API-Call-Remaining and, only on a cap that resets, X-API-Call-Reset. A key without a cap receives none of these headers, so treat all three as optional. When a capped key is used up, the reference documents 429 quota_exceeded, which retrying does not clear: it lifts only when the quota period rolls over or we raise the cap.
06 What else exists
Two more endpoint families, and what is not built
POST /v1/normalize/propertiesis an import helper. Send up to 100 spreadsheet rows with whatever column names you have; it uses a language model to return each row as a project name, address and asset type with a confidence rating. It does not look up incentives. The asset type comes back in its display form, such asCommercial (Multi-Family), which step 1 also accepts./v1/data-submissionsis for partners who send program data to our catalog. It is documented in the reference.- Not available today: an endpoint to list or search the catalog (start from an address, step 1), test-mode or sandbox keys, and webhooks or change notifications.
07 Going live
Before production, and when something is wrong
The reference labels the API early access: response shapes may still change. Parse responses leniently, ignoring fields you do not recognise, and tell us before you go to production.
For a key, a question, or a response that looks wrong, use the contact form and choose API access. Include the request_id.