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" } ] } }
! means required — zipCode: String! must be supplied, startDate: Date may be left out. Leave out a required argument and the query fails validation before it reaches our data, with an error naming the argument. Every query also states its required arguments in words directly beneath the signature, so you never have to count punctuation. On return types the same ! means the field is never null: [Utility!] is a list that may itself be null but never contains nulls.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 — omitting # it is identical to sending 1000. priceType: String # DEFAULT "SEASONALIZED" — omitting it is identical to # sending "SEASONALIZED". That 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!]!
Required: none — every argument is optional. Optional: zipCode, utilityId, monthlyUsage, priceType.
{
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!]
Required: zipCode. Optional: annualUsageInkWh, isMoveIn, startDate.
{
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 monthlyUsage: Long # kWh/mo you already know for this shopper. Carried into # the link as annual_usage (monthlyUsage × 12) so checkout # prices at their real usage instead of the 1000 kWh default. priceType: String # "SEASONALIZED" (default) or "FLAT" — same meaning as on # residentialPlans. "FLAT" also switches checkout to a flat price. ): 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!
residentialEnrollmentUrl — Required: planId, zipCode. Optional: startDate, utilityAccountNumber, customer, address, monthlyUsage, priceType.
businessEnrollmentUrl — Required: planId, zipCode, isMoveIn. Optional: startDate, customer, utilityAccountNumber, address, annualUsageInkWh, businessName, position. isMoveIn is required here but optional on businessPlans and nextStartDate — the one asymmetry in the API, and an easy one to miss. Pass true for new service, false for a switch.
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.monthlyUsage and checkout prices the plan at that figure instead of the 1000 kWh default — the customer sees their own estimated bill on the very first screen. The link picks up two parameters: annual_usage, which is monthlyUsage × 12, and usage_source=EXTERNAL, which tells checkout the number came from you. Adding priceType: "FLAT" adds is_flat=true as well. Both parameters land on enrollmentUrl and headlessEnrollmentUrl alike, and both are optional — omit monthlyUsage and the link behaves exactly as it did before. Residential only. The business equivalent is annualUsageInkWh, which is already an annual figure and is not multiplied — and it behaves differently when you leave it out. Omit annualUsageInkWh from a businessEnrollmentUrl call and the link is built with annual_usage=30000.0, a 30,000 kWh/year stand-in, not with no usage at all. Passing an explicit null is not the same thing: that leaves annual_usage off the link entirely. If you do not know the business's usage, decide deliberately which of those two you want.planId names a plan that exists, or that zipCode is a real ZIP. Pass nonsense for either and you still get a well-formed URL back and no error. Take the plan id from the plan-search response immediately before the call and keep its ZIP with it, and never treat "no GraphQL error" as proof the link will enroll anybody. Plan ids are regenerated when the catalogue refreshes, so a link you stored earlier may point at a plan that no longer exists.priceType defaults to SEASONALIZED, and monthlyUsage defaults to 1000 kWh a month — so every price you see when you pass neither is a seasonalized price calculated at 1000 kWh. Omitting an argument and sending its default produce identical results. On residentialEnrollmentUrl, leaving monthlyUsage off means the link carries no annual_usage and no usage_source at all, and checkout falls back to its own 1000 kWh estimate; that is the behaviour to expect when you do not know the shopper's usage.priceType is matched case-insensitively for flat pricing — "FLAT", "flat" and "Flat" all select it. Any value it does not recognise, such as "fixed", is dropped with no error and you get the seasonalized result back. It is a plain string rather than an enum, so nothing tells you the value was ignored: check the returned URL for is_flat=true when you need to confirm flat pricing was applied. monthlyUsage is a Long, so 1625 is fine but 1625.5 is rejected outright. Check the URL for usage_source=EXTERNAL to confirm the usage was taken.{
residentialEnrollmentUrl(
planId: "02012ac2-73f7-45d6-a08e-38372d49d3b3"
zipCode: "75201"
startDate: "2026-09-01"
utilityAccountNumber: "10443720009406225"
monthlyUsage: 1625
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=02012ac2-73f7-45d6-a08e-38372d49d3b3&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&annual_usage=19500.0&usage_source=EXTERNAL",
"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=02012ac2-73f7-45d6-a08e-38372d49d3b3&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&annual_usage=19500.0&usage_source=EXTERNAL"
}
}
}
{
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 carry annual_usage and usage_source too when you pass monthlyUsage, plus is_flat when you pass priceType: "FLAT"; residential still has no move-in vs. switch flag.
utilities
The utility/TDU(s) serving a zip code — optionally with their residential plans.
utilities(zipCode: String!): [Utility!]
Required: zipCode. There are no optional arguments.
{
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!
Required: zipCode. Optional: isMoveIn.
{
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!]
Required: none — every argument is optional. Optional: street, street2, city, state, zipCode, accountNumber, isBusiness.
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
Required: stateCode, isBusiness. There are no optional arguments.
{
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.