GraphQL · U.S. retail electricity

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.

One endpoint, ask for what you need
GraphQL exposes every query at a single URL. Send a query naming exactly the fields you want; the server returns just those, as JSON. The reference below is generated from the live schema.
Coverage
The API spans the deregulated U.S. electricity markets PowerHQ supports — pass any supported ZIP or state code. The examples on this page use Texas ZIPs (e.g. 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.

EnvironmentEndpoint
Productionhttps://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.

Endpoint note
The API is served from 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" } ] } }
Reading the signatures on this page
Each query below opens with its GraphQL signature. A trailing ! means requiredzipCode: 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.

Example query
{
  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.

Example query
{
  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!

residentialEnrollmentUrlRequired: planId, zipCode. Optional: startDate, utilityAccountNumber, customer, address, monthlyUsage, priceType.

businessEnrollmentUrlRequired: 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.

Identifying the service location
Pass either 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.
Apartments and units
Write the unit as a bare number on the end of 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.
Carrying the shopper's usage into the link
If you already know what the shopper uses, pass 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.
Building a link is not validating a plan
Neither enrollment query checks that 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.
The defaults, when you pass neither
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.
Two things to know about these two arguments
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.
Example query — residential, by utility account number
{
  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"
    }
  }
}
Example query — business, by address
{
  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"
    }
  }
}
Not sure whether you need this?
If you are happy sending the customer to a plan's existing 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.

Example query
{
  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.

Example query
{
  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.

Writing the service address
Put the apartment or unit number on the end of 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.
Example query
{
  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.

Example query
{
  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.
Outside Texas, all-in ≠ the total bill
Because non-TX rates exclude TDU delivery (billed separately by the utility), a customer's total cost is the all-in supply rate plus the utility's delivery charges. In Texas, TDU is already included in both rates.
Only one rate field ignores priceType
advertisedPriceUsdPerKwh 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.
Why all-in exists
Plan structures can be tricky — bill credits, fixed monthly fees — and the all-in rate folds those in so a shopper compares a real per-kWh number, where a bare advertised figure can mislead. The two are equal only on plans with no such adjustments. Use 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"
}
Enrollment & attribution
Each plan returns 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.
Disclosure documents vary by state — and must be shown
The types in 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

FieldTypeNotes
idID!Stable; matches ingested plan id
title / descriptionString!
priceFloat!$/kWh at the requested monthlyUsage (seasonalized)
termInt!Contract length, months
renewablePercentageInt!
rateTypeRateTypeFIXED · 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)
supplierSupplier!
supplierScoresSupplierScoresNullable; sourced separately from pricing
earlyTerminationFeeUsd / …Type / isEarlyTerminationPenalizedFloat / enum / Boolean!
isBillCreditPlan · isPetFriendly · timeOfUseBoolean!
minimumStartDate / maximumStartDateDateNullable
tags[PlanTag!]!
utilityCode / stateCodeStringTDU / state; nullable
createdAtDateTime
enrollmentUrl / headlessEnrollmentUrlString!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 }
What rules is for
feeBreakdown[].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 }
}
The shape of data on error depends on the field
GraphQL nullability decides whether the failed field is nulled or the whole data key disappears. Handle both.
FieldSignatureOn error
residentialPlans[ResidentialPlan!]!no data key at all
utilities[Utility!]{"data":{"utilities":null}}
businessPlans[BusinessPlan!]{"data":{"businessPlans":null}}
Reading 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[] and tags[] can legitimately be empty arrays.
  • feeBreakdown[].rules is null, not an empty array, on almost every fee. Across 74 plans in 75201 it came back null 238 times and [] zero times. Null-check before iterating.
  • supplierScores is nullable when a supplier has no ratings on file, and individual categories inside it may be null independently.
  • earlyTerminationFeeUsd, earlyTerminationFeeType, minimumStartDate, maximumStartDate, utilityCode, stateCode and createdAt are nullable; absence is not an error.
  • Plan id values regenerate on each refresh. Do not persist them, and do not match plans by id across calls.