Plan Data API
A single GraphQL API for U.S. retail electricity across the deregulated markets PowerHQ supports — residential & business plans, component-level pricing, disclosure documents (EFL, TOS, YRAC, and more), utility (TDU) lookup, and enrollment hand-off.
75231, 77002), but the same queries work for other supported markets (e.g. PA 19103, IL 60602, NJ 07030). Plan availability varies by market.API Access
You need an API key. Contact your PowerHQ representative to get one. IP allow-listing is no longer required — the key alone authenticates the request, so you can call the API from anywhere.
| Environment | Endpoint |
|---|---|
| Production | https://eapi.prod.powerhq.co/graphql |
| Certification (staging) | https://eapi.cert.powerhq.co/graphql |
Pass your key in the x-api-key header. Requests without a valid key receive 403 Forbidden.
powerhq.co. If you previously integrated against eapi.energybot.com, that endpoint keeps working during the transition — new integrations should use the powerhq.co endpoints. Enrollment URLs returned by the API resolve to powerhq.co.Making a request
Any GraphQL client works. A minimal example with curl:
$ curl -s -X POST https://eapi.prod.powerhq.co/graphql \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"query": "{ utilities(zipCode: \"75231\") { id name } }"}'
{ "data": { "utilities": [ { "id": "ONCOR", "name": "Oncor" } ] } }
Queries
residentialPlans
Returns residential plans for a location. Provide zipCode or utilityId — if both are given, utilityId wins; if neither resolves, an empty list is returned (not an error).
residentialPlans( zipCode: String # e.g. "75231" utilityId: ID # e.g. "ONCOR" — takes precedence monthlyUsage: Long # kWh/mo the price is calculated at (default 1000) priceType: String # Default "SEASONALIZED": spreads monthlyUsage across a real # 12-month shape (higher summer, lower spring/autumn) and bills # each month at that month's usage, so a bill credit only counts # in the months the customer would actually qualify for it. # "FLAT": same usage every month, so credits always apply. # Affects price, allInRateUsdPerKwh and avgMonthlyBillUsd. ): [ResidentialPlan!]!
{
residentialPlans(zipCode: "77002", monthlyUsage: 1000) {
title
price
term
rateType
supplier { name }
rates { usageKwh advertisedPriceUsdPerKwh allInRateUsdPerKwh avgMonthlyBillUsd }
feeBreakdown { feeType amountUsd applicability threshold }
supplierScores { powerHqRating plansAndRates customerService renewablePlans pucRating }
headlessEnrollmentUrl
}
}
Show response
{
"data": {
"residentialPlans": [
{
"title": "12 Month Usage Bill Credit",
"price": 0.134,
"term": 12,
"rateType": "FIXED",
"supplier": { "name": "Constellation NewEnergy, Inc." },
"rates": [
{ "usageKwh": 500, "advertisedPriceUsdPerKwh": 0.151, "allInRateUsdPerKwh": 0.15066, "avgMonthlyBillUsd": 75.33 },
{ "usageKwh": 1000, "advertisedPriceUsdPerKwh": 0.111, "allInRateUsdPerKwh": 0.13415, "avgMonthlyBillUsd": 134.15 },
{ "usageKwh": 2000, "advertisedPriceUsdPerKwh": 0.119, "allInRateUsdPerKwh": 0.12342, "avgMonthlyBillUsd": 246.83 }
],
"feeBreakdown": [
{ "feeType": "UTILITY_PASS_THRU", "amountUsd": 4.9, "applicability": "MONTHLY", "threshold": null },
{ "feeType": "UTILITY_PASS_THRU", "amountUsd": 0.051, "applicability": "PER_KWH", "threshold": null },
{ "feeType": "BILL_CREDIT", "amountUsd": 35.0, "applicability": "CREDIT_MONTHLY_ABOVE_USAGE", "threshold": 1000.0 },
{ "feeType": "BILL_CREDIT", "amountUsd": 15.0, "applicability": "CREDIT_MONTHLY_ABOVE_USAGE", "threshold": 2000.0 }
],
"supplierScores": { "powerHqRating": 4.7, "plansAndRates": 4.0, "customerService": 5.0, "renewablePlans": 2.0, "pucRating": 5.0 },
"headlessEnrollmentUrl": "https://www.cert.powerhq.co/partner-app.html?utm_source=YOUR_PARTNER_CODE&utm_medium=referral&utm_campaign=partners#/redirect?ftype=RESIDENTIAL_PARTNER&plan_id=7a8c1d9f-f83a-4322-850f-c6939ccf4fb3&zip_code=77002&ref=YOUR_PARTNER_CODE"
}
]
}
}
businessPlans
Commercial plans for a zip code. zipCode required; annualUsageInkWh selects the pricing assumption. For new service set isMoveIn: true (default false = switch). A startDate earlier than the resolved minimum returns a validation error.
businessPlans(zipCode: String!, annualUsageInkWh: Float, isMoveIn: Boolean, startDate: Date): [BusinessPlan!]
{
businessPlans(zipCode: "75070", annualUsageInkWh: 23000, isMoveIn: true) {
id
term
price
monthlyFee
supplier { name }
enrollmentUrl
agreementUrl
}
}
Show response
{
"data": {
"businessPlans": [
{
"id": "58b2e47e-91d7-4b72-9b5d-9bc9136613d8",
"term": 3,
"price": 0.0705,
"monthlyFee": 4.95,
"supplier": { "name": "NRG Energy, Inc." },
"enrollmentUrl": "https://www.cert.powerhq.co/app.html?utm_source=YOUR_PARTNER_CODE&utm_medium=referral&utm_campaign=partners#/redirect?ftype=BUSINESS_PARTNER&plan_id=58b2e47e-91d7-4b72-9b5d-9bc9136613d8&zip_code=75070&start_date=2026-08-06&annual_usage=23000.0&is_move_in=true&ref=YOUR_PARTNER_CODE",
"agreementUrl": "https://www.cert.powerhq.co/api/document/sample/template/v3/AgreementPreview.pdf?key=TX/NRG/ONCOR&type=supplier_contract"
}
]
}
}
residentialEnrollmentUrl / businessEnrollmentUrl Optional
You do not need these queries to enroll a customer. Every plan returned by residentialPlans and businessPlans already carries a working enrollmentUrl and headlessEnrollmentUrl. Use those and you are done.
Reach for these two queries only when you want to add shopper detail to the link after the plan search — a name, email, phone, service location or start date you collected once the shopper had already picked a plan. They return the same two links, with that context attached so the customer does not re-enter it.
residentialEnrollmentUrl( planId: String! zipCode: String! startDate: Date utilityAccountNumber: String customer: EnrollmentCustomerInput address: EnrollmentAddressInput ): EnrollmentUrl! businessEnrollmentUrl( planId: String! zipCode: String! isMoveIn: Boolean! annualUsageInkWh: Float startDate: Date utilityAccountNumber: String customer: EnrollmentCustomerInput address: EnrollmentAddressInput businessName: String position: String # the shopper's role at the business ): EnrollmentUrl!
utilityAccountNumber or address. Only one is used for the lookup, and utilityAccountNumber takes precedence if you supply both. When using address, provide street, city and state; if the address does not resolve you get a link back with no service location and no error, so check that the returned URL contains prospect_id. Use utilityAccounts to look up an account number from an address first.street ("1441 East St 315") and never send street2. #, unit, APT and Ste prefixes do not match. Even then, a multi-unit building often will not resolve from the address alone — for apartments and units, look the account up with utilityAccounts and pass utilityAccountNumber instead. That resolves reliably where the address does not.{
residentialEnrollmentUrl(
planId: "51ef348d-280a-451a-9d04-1d9593dccc8d"
zipCode: "75201"
startDate: "2026-09-01"
utilityAccountNumber: "10443720009406225"
customer: { firstName: "Jane", lastName: "Doe", email: "jane@example.com", phone: "2145550100" }
) {
enrollmentUrl
headlessEnrollmentUrl
}
}
Show response
{
"data": {
"residentialEnrollmentUrl": {
"enrollmentUrl": "https://www.cert.powerhq.co/app.html?utm_source=YOUR_PARTNER_CODE&utm_medium=referral&utm_campaign=partners#/redirect?ftype=RESIDENTIAL_PARTNER&plan_id=51ef348d-280a-451a-9d04-1d9593dccc8d&zip_code=75201&prospect_id=v2-A373483C2E2B04230479696DD8EB7E11&state=TX&start_date=2026-09-01&ref=YOUR_PARTNER_CODE&fname=Jane&lname=Doe&email=jane%40example.com&phone=2145550100",
"headlessEnrollmentUrl": "https://www.cert.powerhq.co/partner-app.html?utm_source=YOUR_PARTNER_CODE&utm_medium=referral&utm_campaign=partners#/redirect?ftype=RESIDENTIAL_PARTNER&plan_id=51ef348d-280a-451a-9d04-1d9593dccc8d&zip_code=75201&prospect_id=v2-A373483C2E2B04230479696DD8EB7E11&state=TX&start_date=2026-09-01&ref=YOUR_PARTNER_CODE&fname=Jane&lname=Doe&email=jane%40example.com&phone=2145550100"
}
}
}
{
businessEnrollmentUrl(
planId: "860b4865-1025-46c1-95c7-d783befcef1c"
zipCode: "75220"
isMoveIn: false
annualUsageInkWh: 63965
startDate: "2026-09-01"
businessName: "Acme Roofing"
position: "Owner"
customer: { firstName: "Jane", lastName: "Doe", email: "jane@acmeroofing.com", phone: "2145550100" }
address: { street: "10590 King William Dr", city: "Dallas", state: "TX" }
) {
enrollmentUrl
headlessEnrollmentUrl
}
}
Show response
{
"data": {
"businessEnrollmentUrl": {
"enrollmentUrl": "https://www.cert.powerhq.co/app.html?utm_source=YOUR_PARTNER_CODE&utm_medium=referral&utm_campaign=partners#/redirect?ftype=BUSINESS_PARTNER&plan_id=860b4865-1025-46c1-95c7-d783befcef1c&zip_code=75220&prospect_id=v2-4F17BC6F325BBB9F921B183CCCAE2DF8&state=TX&start_date=2026-09-01&ref=YOUR_PARTNER_CODE&fname=Jane&lname=Doe&email=jane%40acmeroofing.com&phone=2145550100&annual_usage=63965.0&is_move_in=false&bname=Acme+Roofing&position=Owner",
"headlessEnrollmentUrl": "https://www.cert.powerhq.co/partner-app.html?utm_source=YOUR_PARTNER_CODE&utm_medium=referral&utm_campaign=partners#/redirect?ftype=BUSINESS_PARTNER&plan_id=860b4865-1025-46c1-95c7-d783befcef1c&zip_code=75220&prospect_id=v2-4F17BC6F325BBB9F921B183CCCAE2DF8&state=TX&start_date=2026-09-01&ref=YOUR_PARTNER_CODE&fname=Jane&lname=Doe&email=jane%40acmeroofing.com&phone=2145550100&annual_usage=63965.0&is_move_in=false&bname=Acme+Roofing&position=Owner"
}
}
}
enrollmentUrl, you do not need these queries at all. They exist purely to save the shopper from retyping details you already have.What carries into the link. Residential: customer name, email, phone, service location and start date. Business: all of those plus annual_usage, is_move_in, bname and position. Residential links do not carry usage or move-in vs. switch.
utilities
The utility/TDU(s) serving a zip code — optionally with their residential plans.
utilities(zipCode: String!): [Utility!]
{
utilities(zipCode: "75231") {
id
name
}
}
Show response
{
"data": {
"utilities": [
{ "id": "ONCOR", "name": "Oncor" }
]
}
}
nextStartDate
Earliest available service start date for a zip code.
nextStartDate(zipCode: String!, isMoveIn: Boolean): PlanNextStartDate!
{
nextStartDate(zipCode: "75070", isMoveIn: false) {
nextStartDate
}
}
Show response
{
"data": {
"nextStartDate": { "nextStartDate": "2026-08-04" }
}
}
utilityAccounts
Search utility service accounts by account number and/or address. Either street or accountNumber is required.
utilityAccounts(street: String, street2: String, city: String, state: String, zipCode: String, accountNumber: String, isBusiness: Boolean): [UtilityAccount!]
street as a bare number, with nothing in front of it. "1441 East St 315" matches. "#315", "unit 315", "Unit 315", "APT 315" and "Ste 315" all return nothing. Do not send street2 at all — any value, including an empty string, causes the lookup to miss. Omit the unit entirely and you get every account in the building.{
utilityAccounts(street: "947 STREETLIGHT STLG 250HPS", city: "BROWNSVILLE", state: "TX", zipCode: "78521") {
id
accountNumber
utilityCode
isBusiness
accountStatus
displayAddress
serviceAddress { street street2 city state zipCode }
}
}
Show response
{
"data": {
"utilityAccounts": [
{
"id": "v2-CB1C1D302FC4B19F58D4F89F775AAFD6",
"accountNumber": "10032789448946468",
"utilityCode": "AEPCC",
"isBusiness": true,
"accountStatus": "ACTIVE",
"displayAddress": "947 STREETLIGHT STLG 250HPS, CAMERON COUNTY ST LIGHTS, BROWNSVILLE, TX",
"serviceAddress": { "street": "947 STREETLIGHT STLG 250HPS", "street2": " CAMERON COUNTY ST LIGHTS", "city": "BROWNSVILLE", "state": "TX", "zipCode": "78521" }
}
]
}
}
energyInfoByState
Average rate and generation mix for a state. isBusiness: true = business, false = residential.
energyInfoByState(stateCode: String!, isBusiness: Boolean!): EnergyInfo
{
energyInfoByState(stateCode: "TX", isBusiness: false) {
averagePrice
stateName
electricityGenerationPercentage { renewableGeneration nonRenewableGeneration }
}
}
Show response
{
"data": {
"energyInfoByState": {
"averagePrice": 16.44,
"stateName": "Texas",
"electricityGenerationPercentage": { "renewableGeneration": 34, "nonRenewableGeneration": 66 }
}
}
}
Reading pricing: advertised vs. all-in
Each plan's rates[] returns, at 500 / 1,000 / 2,000 kWh, an advertised rate and an all-in rate (both per kWh), plus the estimated monthly bill. What those rates include depends on the market.
Texas
- Both rates include TDU delivery charges.
advertisedPriceUsdPerKwh— effective rate at a static, standard usage (a flat 500 / 1,000 / 2,000 kWh every month).allInRateUsdPerKwh— the actual bill on the customer's usage, seasonalized by default.
All other states (PA, OH, IL, NJ, …)
- Neither rate includes TDU delivery — the utility bills delivery separately.
advertisedPriceUsdPerKwh— the variable supply rate per kWh only (excludes any fixed monthly fee).allInRateUsdPerKwh— the supply rate plus the fixed monthly fee (amortized per kWh); still excludes TDU.
priceTypeadvertisedPriceUsdPerKwh never changes with priceType. It is always the supplier's flat display price, the same figure shown on the EFL, whether you request FLAT or seasonalized. Only allInRateUsdPerKwh, avgMonthlyBillUsd and the top-level price respond. priceType is a request-wide setting, not a per-field toggle.allInRateUsdPerKwh (and the seasonalized top-level price) for the effective supply cost. avgMonthlyBillUsd is the estimated bill at each usage level.Example — residential plan query
{
residentialPlans(zipCode: "77002", monthlyUsage: 1000) {
title
rateType
term
supplier { name shortName registrationId }
rates { usageKwh advertisedPriceUsdPerKwh allInRateUsdPerKwh avgMonthlyBillUsd }
feeBreakdown { feeType amountUsd applicability }
documents { type url }
earlyTerminationFeeUsd
isBillCreditPlan
supplierScores { powerHqRating plansAndRates customerService renewablePlans pucRating }
headlessEnrollmentUrl
}
}
{
"title": "12 Month (No Min Usage Fee)",
"rateType": "FIXED", "term": 12,
"supplier": { "name": "Constellation NewEnergy, Inc.", "shortName": "Constellation", "registrationId": "10014" },
"rates": [
{ "usageKwh": 500, "advertisedPriceUsdPerKwh": 0.143, "allInRateUsdPerKwh": 0.1428, "avgMonthlyBillUsd": 71.4 },
{ "usageKwh": 1000, "advertisedPriceUsdPerKwh": 0.138, "allInRateUsdPerKwh": 0.1379, "avgMonthlyBillUsd": 137.9 },
{ "usageKwh": 2000, "advertisedPriceUsdPerKwh": 0.136, "allInRateUsdPerKwh": 0.13545, "avgMonthlyBillUsd": 270.9 }
],
"feeBreakdown": [
{ "feeType": "UTILITY_PASS_THRU", "amountUsd": 4.9, "applicability": "MONTHLY" },
{ "feeType": "UTILITY_PASS_THRU", "amountUsd": 0.051, "applicability": "PER_KWH" }
],
"documents": [
{ "type": "EFL", "url": "https://www.constellation.com/bin/residential/GetContractVersionPDF?versionNum=4972691" },
{ "type": "TOS", "url": "https://www.constellation.com/bin/residential/GetContractVersionPDF?versionNum=4977905" },
{ "type": "YRAC", "url": "https://www.constellation.com/bin/residential/GetContractVersionPDF?versionNum=4977876" }
],
"earlyTerminationFeeUsd": 150.0,
"isBillCreditPlan": false,
"supplierScores": { "powerHqRating": 4.7, "plansAndRates": 4.0, "customerService": 5.0, "renewablePlans": 2.0, "pucRating": 5.0 },
"headlessEnrollmentUrl": "https://www.cert.powerhq.co/partner-app.html?utm_source=YOUR_PARTNER_CODE&utm_medium=referral&utm_campaign=partners#/redirect?ftype=RESIDENTIAL_PARTNER&plan_id=7a8c1d9f-f83a-4322-850f-c6939ccf4fb3&zip_code=77002&ref=YOUR_PARTNER_CODE"
}
enrollmentUrl (hosted) and headlessEnrollmentUrl (embeddable). Both are stamped with your partner code (utm_source / ref) so conversions attribute to you. An ENROLLMENT document, when present, links to the supplier's own site — not a PowerHQ flow.documents[] vary by market and by supplier. Texas plans typically return EFL (Electricity Facts Label), TOS and YRAC. Outside Texas the mix is inconsistent: in Pennsylvania (19103), of 14 plans, 9 return only TOS or TOS + ENROLLMENT, 4 include CONTRACT_SUMMARY, and one supplier returns a full Texas-style EFL + YRAC + TOS set. Never assume a given type is present, and never assume it is absent — read documents[] per plan. These disclosure documents must be accessible to the customer in your UI whenever a plan is displayed (and before enrollment) — surface the relevant documents[] link(s) for each plan.Types & enums
ResidentialPlan
| Field | Type | Notes |
|---|---|---|
id | ID! | Stable; matches ingested plan id |
title / description | String! | |
price | Float! | $/kWh at the requested monthlyUsage (seasonalized) |
term | Int! | Contract length, months |
renewablePercentage | Int! | |
rateType | RateType | FIXED · VARIABLE · PREPAID · SUBSCRIPTION |
rates | [PlanRate!]! | Advertised + all-in + est. bill at 500/1000/2000 |
feeBreakdown | [PlanFee!]! | Component charges & credits |
documents | [PlanDocument!]! | EFL / TOS / YRAC / ENROLLMENT links (may be empty) |
supplier | Supplier! | |
supplierScores | SupplierScores | Nullable; sourced separately from pricing |
earlyTerminationFeeUsd / …Type / isEarlyTerminationPenalized | Float / enum / Boolean! | |
isBillCreditPlan · isPetFriendly · timeOfUse | Boolean! | |
minimumStartDate / maximumStartDate | Date | Nullable |
tags | [PlanTag!]! | |
utilityCode / stateCode | String | TDU / state; nullable |
createdAt | DateTime | |
enrollmentUrl / headlessEnrollmentUrl | String! | Hosted / embeddable hand-off |
Supporting object types
type PlanRate { usageKwh: Int! advertisedPriceUsdPerKwh: Float! allInRateUsdPerKwh: Float! avgMonthlyBillUsd: Float! } # advertisedPriceUsdPerKwh is fixed regardless of priceType type PlanFee { feeType: FeeType! amountUsd: Float! applicability: FeeApplicability! threshold: Float rules: [RuleEntry!] } type RuleEntry { key: String! value: String! } # qualifying condition on a fee; almost always null type PlanDocument { type: LinkType! url: String! title: String } type Supplier { id: ID! name: String! shortName: String logoUrl: String! registrationId: String } type SupplierScores { powerHqRating: Float plansAndRates: Float customerService: Float renewablePlans: Float pucRating: Float } type PlanTag { key: String! value: String label: String reasons: [String!] } type BusinessPlan { id: ID! term: Int! price: Float! monthlyFee: Float! renewablePercentage: Int! supplier: Supplier! agreementUrl: String! enrollmentUrl: String! headlessEnrollmentUrl: String! } type Utility { id: ID! name: String! residentialPlans: [ResidentialPlan!] } type UtilityAccount { id: ID! accountNumber: String accountStatus: AccountStatus serviceAddress: Address! displayAddress: String isBusiness: Boolean! utilityCode: String! } type EnergyInfo { averagePrice: Float! stateCode: String! stateName: String! electricityGenerationPercentage: ElectricityGenerationPercentage } # averagePrice is CENTS per kWh (e.g. 16.44), unlike plan price which is DOLLARS per kWh type EnrollmentUrl { enrollmentUrl: String! headlessEnrollmentUrl: String! } input EnrollmentCustomerInput { firstName: String lastName: String email: String phone: String } input EnrollmentAddressInput { street: String street2: String city: String state: String }
rules is forfeeBreakdown[].rules carries a qualifying condition on a fee, for the rare plan where the fee only applies to certain customers. It is null on almost every fee. The one populated case in Texas today is Octopus Energy's "Octo EV 12", where the per-kWh credit only applies to EV owners:
{ "feeType": "CREDIT_PER_KWH", "applicability": "DEVICE", "rules": [ { "key": "ELECTRIC_DEVICE_TYPE", "value": "EV" } ] }Enums
RateType FIXEDVARIABLEPREPAIDSUBSCRIPTION
LinkType EFLTOSYRACENROLLMENTPREPAID_DISCLOSURE_STATEMENTCONTRACT_SUMMARYENVIRONMENTAL_DISCLOSUREARBITRATION_ADDENDUMCOMM_POLICYPAYMENT_TERMS
FeeApplicability PER_KWHMONTHLYPER_KWH_BELOW_USAGEMONTHLY_BELOW_USAGEMONTHLY_ABOVE_USAGECREDIT_MONTHLY_ABOVE_USAGECREDIT_MONTHLY_BELOW_USAGESCHEDULEDEVICEPER_KW
EarlyTerminationFeeType FIXEDPER_MONTH_REMAINING_PERIODNONEUNKNOWN
AccountStatus ACTIVEINACTIVEDE_ENERGIZED
FeeType includes residential values (ENERGY_CHARGE, UTILITY_PASS_THRU, BASE_CHARGE, BILL_CREDIT, MIN/MAX_USAGE_CHARGE, CREDIT_PER_KWH, UPCHARGE_PER_KWH) plus commercial BUS_* charges.
Errors
Query and validation problems return HTTP 200 with an errors[] array — not a REST-style error envelope:
{
"errors": [ {
"message": "Exception while fetching data (/businessPlans) : Invalid zipcode 1234",
"locations": [ { "line": 1, "column": 3 } ],
"path": [ "businessPlans" ]
} ],
"data": { "businessPlans": null }
}
data on error depends on the fielddata key disappears. Handle both.
| Field | Signature | On error |
|---|---|---|
residentialPlans | [ResidentialPlan!]! | no data key at all |
utilities | [Utility!] | {"data":{"utilities":null}} |
businessPlans | [BusinessPlan!] | {"data":{"businessPlans":null}} |
response.data.residentialPlans without checking errors first will throw rather than return null.Transport/auth failures return an HTTP status: 403 (missing or invalid key), 400 (malformed request). A utilityAccounts query with neither street nor accountNumber returns "No street address or account number specified for utility accounts search".
Field-level notes
documents[]andtags[]can legitimately be empty arrays.feeBreakdown[].rulesisnull, not an empty array, on almost every fee. Across 74 plans in 75201 it came backnull238 times and[]zero times. Null-check before iterating.supplierScoresis nullable when a supplier has no ratings on file, and individual categories inside it may be null independently.earlyTerminationFeeUsd,earlyTerminationFeeType,minimumStartDate,maximumStartDate,utilityCode,stateCodeandcreatedAtare nullable; absence is not an error.- Plan
idvalues regenerate on each refresh. Do not persist them, and do not match plans byidacross calls.