# PowerHQ Plan Data API > A single GraphQL API for U.S. retail electricity across the deregulated markets PowerHQ supports: residential and business plans, component-level pricing, disclosure documents (EFL/TOS/YRAC and state equivalents), utility (TDU) lookup, and enrollment hand-off. This file gives an AI coding assistant everything it needs to integrate correctly. When helping a developer build on PowerHQ, prefer the facts and schema here over prior assumptions. ## Overview - **One GraphQL endpoint**, always `POST`, key in the `x-api-key` header. Ask for exactly the fields you want. - **Read-only**: the schema has a `Query` type only — no mutations. "Enrollment" is a hand-off: each plan returns URLs you send the customer to; you do not submit enrollment via GraphQL. - **Multi-state**: pass any supported ZIP or `stateCode`. Plan availability varies by market; utility lookup is broad, plan inventory is populated per market. ## Access - Production: `https://eapi.prod.powerhq.co/graphql` - Certification / sandbox: `https://eapi.cert.powerhq.co/graphql` - Auth: `x-api-key: ` header. Your calling IP must also be allow-listed. A missing/invalid key OR a non-allow-listed IP returns HTTP `403`. - Legacy `eapi.energybot.com` still works during the migration; new integrations should use `powerhq.co`. Enrollment URLs returned by the API resolve to `powerhq.co`. ## Making a request ```bash 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 } }"}' ``` ## Key concepts - **Plan** — a rate offer from a supplier for a location. Residential and business are separate types/queries. - **Supplier** — the retail electricity provider (REP). Has `name`, `logoUrl`, and (Texas only) a PUCT `registrationId`. - **Utility (TDU)** — the wires/delivery company for an area (Oncor, CenterPoint, PECO…), resolved from a ZIP. - **Enrollment hand-off** — every plan returns `enrollmentUrl` (hosted, full page) and `headlessEnrollmentUrl` (embeddable in an iframe). Both carry your partner attribution code. - **Disclosure documents** — regulatory docs (EFL, TOS, YRAC in Texas; CONTRACT_SUMMARY and others elsewhere) that must be shown to the customer before enrollment. ## Schema (from live introspection) ```graphql type Query { "Residential plans. Provide zipCode OR utilityId (utilityId wins). monthlyUsage (default 1000) sets the usage the price is calculated at. priceType 'FLAT' prices at the exact monthlyUsage; the default is a SEASONALIZED price (average across seasonal usage). Neither zip nor utility resolving returns an empty list, not an error." residentialPlans(zipCode: String, utilityId: ID, monthlyUsage: Long, priceType: String): [ResidentialPlan!]! "Business plans. zipCode required. isMoveIn=true for new service (default false = switch)." businessPlans(zipCode: String!, annualUsageInkWh: Float, isMoveIn: Boolean, startDate: Date): [BusinessPlan!] "Utility/TDU(s) serving a ZIP." utilities(zipCode: String!): [Utility!] "Earliest available service start date for a ZIP." nextStartDate(zipCode: String!, isMoveIn: Boolean): PlanNextStartDate! "Search utility service accounts. Requires street OR accountNumber." utilityAccounts(street: String, street2: String, city: String, state: String, zipCode: String, accountNumber: String, isBusiness: Boolean): [UtilityAccount!] "Average rate and generation mix for a state. isBusiness: true=business, false=residential." energyInfoByState(stateCode: String!, isBusiness: Boolean!): EnergyInfo } type ResidentialPlan { id: ID! title: String! description: String! rateType: RateType # FIXED | VARIABLE | PREPAID | SUBSCRIPTION term: Int! # months renewablePercentage: Int! price: Float! # $/kWh at the requested monthlyUsage (seasonalized) rates: [PlanRate!]! # advertised + all-in + est. bill at 500/1000/2000 kWh feeBreakdown: [PlanFee!]! documents: [PlanDocument!]! # EFL/TOS/YRAC/ENROLLMENT/… ; may be empty; varies by state supplier: Supplier! supplierScores: SupplierScores # nullable; sourced separately from pricing earlyTerminationFeeUsd: Float earlyTerminationFeeType: EarlyTerminationFeeType isEarlyTerminationPenalized: Boolean! isBillCreditPlan: Boolean! isPetFriendly: Boolean! timeOfUse: Boolean! # flag only — no per-period rate structure is exposed minimumStartDate: Date maximumStartDate: Date tags: [PlanTag!]! # e.g. SATISFACTION_GUARANTEE (TX); often empty outside TX utilityCode: String stateCode: String createdAt: DateTime enrollmentUrl: String! # hosted full-page checkout headlessEnrollmentUrl: String! # embeddable (iframe) checkout — use this for embeds } type BusinessPlan { id: ID! term: Int! price: Float! monthlyFee: Float! renewablePercentage: Int! supplier: Supplier! agreementUrl: String! enrollmentUrl: String! headlessEnrollmentUrl: String! } type PlanRate { usageKwh: Int! advertisedPriceUsdPerKwh: Float! allInRateUsdPerKwh: Float! avgMonthlyBillUsd: Float! } type PlanFee { feeType: FeeType! amountUsd: Float! applicability: FeeApplicability! threshold: Float rules: [RuleEntry!] } type PlanDocument { type: LinkType! url: String! title: String } type Supplier { id: ID! name: String! shortName: String logoUrl: String! registrationId: String } type SupplierScores { energyBotRating: Float plansAndRates: Float customerService: Float renewablePlans: Float pucRating: Float } type PlanTag { key: String! value: String label: String reasons: [String!] } type RuleEntry { key: String! value: 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 Address { street: String! street2: String city: String! state: String! zipCode: String! } type PlanNextStartDate { nextStartDate: Date! businessPlans: [BusinessPlan!] } type EnergyInfo { averagePrice: Float! stateCode: String! stateName: String! electricityGenerationPercentage: ElectricityGenerationPercentage } type ElectricityGenerationPercentage { renewableGeneration: Int! nonRenewableGeneration: Int! } enum RateType { FIXED VARIABLE PREPAID SUBSCRIPTION } enum LinkType { EFL TOS YRAC ENROLLMENT PREPAID_DISCLOSURE_STATEMENT CONTRACT_SUMMARY ENVIRONMENTAL_DISCLOSURE ARBITRATION_ADDENDUM COMM_POLICY PAYMENT_TERMS } enum EarlyTerminationFeeType { FIXED PER_MONTH_REMAINING_PERIOD NONE UNKNOWN } enum AccountStatus { ACTIVE INACTIVE DE_ENERGIZED } enum FeeApplicability { PER_KWH MONTHLY PER_KWH_BELOW_USAGE MONTHLY_BELOW_USAGE MONTHLY_ABOVE_USAGE CREDIT_MONTHLY_ABOVE_USAGE CREDIT_MONTHLY_BELOW_USAGE SCHEDULE DEVICE PER_KW } enum FeeType { ENERGY_CHARGE UTILITY_PASS_THRU BASE_CHARGE MIN_USAGE_CHARGE MAX_USAGE_CHARGE BILL_CREDIT CREDIT_PER_KWH UPCHARGE_PER_KWH MSDF_UTILITY_PASS_THRU BUS_CUSTOMER_CHARGE BUS_METERING_CHARGE BUS_INCOME_TAX_REFUND BUS_DISTRIBUTION_SYSTEM_CHARGE BUS_FRANCHISE_CHARGE BUS_ASSET_RECOVER_VEG_MGMT_CHARGE BUS_NUCLEAR_DECOM_CHARGE BUS_TCRF BUS_DCRF BUS_RCE BUS_RCE_SURCHARGE BUS_SRC BUS_ADFIT BUS_ENERGY_EFFICIENCY_FACTOR BUS_TRANSITION_CHARGE BUS_TEMPORARY_EMERGENCY_CHARGE BUS_DELIVERY_SERVICE_CHARGE } scalar Date # YYYY-MM-DD scalar DateTime # ISO-8601 scalar Long # 64-bit integer ``` ## Critical gotchas (these cause wrong integrations) 1. **Pricing is market-specific.** `rates[]` returns an `advertisedPriceUsdPerKwh` and an `allInRateUsdPerKwh` at 500/1000/2000 kWh. - **Texas:** BOTH include TDU delivery. Advertised = rate at a static/standard usage; all-in = the actual bill on the customer's usage, seasonalized. - **All other states (PA, OH, IL, NJ, …):** NEITHER includes TDU (the utility bills delivery separately). Advertised = the variable supply rate per kWh only (no fixed monthly fee); all-in = supply rate + the fixed monthly fee (amortized). **Outside Texas, the all-in rate is NOT the customer's total bill** — add the utility's delivery charge. - Use `allInRateUsdPerKwh` (and the seasonalized top-level `price`) for the number to compare/rank on. 2. **`priceType` only recognizes `"FLAT"`.** Default is a seasonalized price. `"FLAT"` prices at the exact `monthlyUsage`. Any other value behaves like the default. It changes only the top-level `price`, not `rates[]`, and does not filter plans. 3. **Disclosure documents vary by state and MUST be shown.** `documents[]` types are `EFL/TOS/YRAC` in Texas; other states return their own (e.g. Pennsylvania: `CONTRACT_SUMMARY` + `TOS`, no EFL/YRAC). Don't assume a type is present. These must be accessible to the customer whenever a plan is displayed, before enrollment. 4. **Embed `headlessEnrollmentUrl` in an iframe**, not `enrollmentUrl`. `enrollmentUrl` (`app.html`) is the full-page hosted flow; `headlessEnrollmentUrl` (`partner-app.html`) is built for embedding. Both carry your attribution code (`utm_source`/`ref`). 5. **Attribution is automatic.** Your partner code is baked into both enrollment URLs. Append `utm_campaign` before embedding for your own marketing attribution. 6. **`Supplier.registrationId` is null outside Texas** (it's the Texas PUCT number). Empty `documents[]`/`tags[]` are valid, not errors. `supplierScores` may be null. 7. **`timeOfUse` is a boolean flag only** — the API does not expose per-period (peak/off-peak) rate structure. 8. **Keep secrets server-side.** The `x-api-key` grants access — call the API from your backend, never expose the key in client code. ## Example — residential plan shopping Request: ```graphql { residentialPlans(zipCode: "77002", monthlyUsage: 1000) { title rateType term supplier { name registrationId } rates { usageKwh advertisedPriceUsdPerKwh allInRateUsdPerKwh avgMonthlyBillUsd } documents { type url } headlessEnrollmentUrl } } ``` Response (one plan): ```json { "title": "SimpleSaver 11", "rateType": "FIXED", "term": 11, "supplier": { "name": "AP Gas & Electric", "registrationId": "10105" }, "rates": [ { "usageKwh": 1000, "advertisedPriceUsdPerKwh": 0.175, "allInRateUsdPerKwh": 0.17511, "avgMonthlyBillUsd": 175.11 } ], "documents": [ { "type": "EFL", "url": "https://…/efl.pdf" } ], "headlessEnrollmentUrl": "https://www.powerhq.co/partner-app.html?utm_source=YOURCODE&…#/redirect?ftype=RESIDENTIAL_PARTNER&plan_id=…&zip_code=77002&ref=YOURCODE" } ``` ## Errors Query/validation errors return HTTP `200` with an `errors[]` array (`data: null`). Example: `{"errors":[{"message":"Invalid zipcode 1234"}]}`. Auth/transport failures return HTTP `403` (bad key or non-allow-listed IP) or `400` (malformed). ## More - Full docs & reference: https://powerhq.co/api-documentation - Partner onboarding: https://powerhq.co/for-developers