Guide to Bayou Energy Webhooks: Features and Best Practices
Bayou Energy logs in to a customer's utility account, with their permission, and returns their bills and interval (meter) data through an API. Energy, solar and efficiency companies use it to qualify customers and track consumption. Bills arrive over time rather than in one response, so Bayou's webhooks tell you when there is something new: a bill has been found or updated, the first pull of bills or intervals for a customer has finished, or a customer's utility login needs attention.
What are Bayou Energy webhooks?
Bayou sends every webhook for a company to one URL, set in the dashboard. There is no event selection, so your receiver gets all seven events and branches on the event field. Each body has two keys: event, and object, which is either a bill or a customer.
Deliveries are signed according to the Standard Webhooks specification, with webhook-id, webhook-timestamp and webhook-signature headers. Bayou is unusually clear that verification is up to you: it sends webhooks whether or not you verify them, and suggests any Standard Webhooks library or a few lines of your own code.
Bayou Energy webhook features
| Feature | Details |
|---|---|
| Configuration | One Webhook URL per company, on the dashboard's Settings page |
| Event selection | None. Every event goes to the URL |
| Signature | Standard Webhooks HMAC-SHA256 |
| Signature header | webhook-signature, with webhook-id and webhook-timestamp |
| Secret | One per company, whsec_..., shown below the Webhook URL field |
| Body | JSON |
| Event routing | Body field event |
| Replay guidance | Reject timestamps more than 5 minutes from the current time |
| Test vector | Published on the verification page |
| Retries, timeout, source IPs | Not documented |
Events and payloads
| Event | object is | Triggered When | Common Use Cases |
|---|---|---|---|
new_bill | Bill | A new bill was discovered | Store the bill, update usage dashboards |
updated_bill | Bill | A bill was updated | Refresh stored bill data |
bills_ready | Customer | The initial bill discovery finished for a customer | Fetch the customer's bills, finish onboarding |
intervals_ready | Customer | The initial interval discovery finished for a customer | Fetch intervals, start load analysis |
customer_ | Customer | A customer successfully shared their utility credentials (not sent if the credentials are marked invalid) | Mark the customer as connected |
new_unparsed_bill | Bill | A bill was discovered but couldn't be parsed yet | Show a "processing" state, wait for updated_bill |
customer_ | Customer | A customer must share their credentials again | Send the customer to onboarding_link |
customer_ explains itself with a reason: credentials_are_invalid when the stored email or password stopped working, or mfa_factor_has_expired when the customer needs to supply a new MFA code. It also includes the link to send them back to:
{
"event": "customer_must_reauthenticate",
"object": {
"id": 12345,
"external_id": "MSCOTT123",
"reason": "credentials_are_invalid",
"onboarding_token": "ABCDEF0123456_ABCDEF0123456_ABCDEF0123456",
"onboarding_link": "https://bayou.energy/companies/12345/sign_up?key=ABCDEF0123456"
}
}
bills_ready carries the customer with a bills_parsed array of full bill records, including billing_period_from, billing_period_to, electricity_consumption, charges and file_url. Payloads have no event ID or event timestamp, so the webhook-id header is the only delivery identifier. Treat file_url, onboarding_token and onboarding_link as secrets and keep them out of logs.
Setting up Bayou Energy webhooks
- Open your company's Settings page in the Bayou dashboard.
- Enter your endpoint in the Webhook URL field and save.
- Copy the Webhook signing secret shown just below it,
whsec_prefix included, and store it asBAYOU_WEBHOOK_SECRET.
You don't need a real utility account to test. Speculoos Power, Bayou's fake utility, has known logins that return fake bills and intervals, and changing a test user's email produces the invalid-credentials case behind customer_.
With Hookdeck, create a source with the Bayou Energy type, paste the signing secret, and put the source URL in the Webhook URL field. During development, the Hookdeck CLI forwards that source to your local server:
npx hookdeck-cli listen 3000 bayou-energy --path /webhooks/bayou-energy
Securing Bayou Energy webhooks
Remove whsec_ from the secret and base64-decode the rest to get the key. Sign <webhook-id>.<webhook-timestamp>.<raw body> with HMAC-SHA256, base64-encode the result, and compare it with each space-separated v1, entry in webhook-signature. One step of Bayou's write-up shows the header as v1 <signature> with a space, but its table and example use v1,, which is what the function below expects:
const crypto = require('node:crypto');
function decodeBase64(value) {
if (typeof value !== 'string' || !/^[A-Za-z0-9+/]+={0,2}$/.test(value)) return null;
const bytes = Buffer.from(value, 'base64');
return bytes.length > 0 && bytes.toString('base64').replace(/=+$/, '') === value.replace(/=+$/, '')
? bytes : null;
}
function verifySignature(rawBody, headers, secret) {
if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
const prefix = headers['webhook-id'] !== undefined ? 'webhook' : 'svix';
const id = headers[`${prefix}-id`];
const timestamp = headers[`${prefix}-timestamp`];
const signature = headers[`${prefix}-signature`];
if (typeof id !== 'string' || !id || typeof timestamp !== 'string' ||
!/^[0-9]+$/.test(timestamp) || typeof signature !== 'string') return false;
const key = decodeBase64(secret.replace(/^whsec_/, ''));
if (!key) return false;
const message = Buffer.concat([Buffer.from(`${id}.${timestamp}.`, 'utf8'), rawBody]);
const candidates = signature.split(/\s+/).filter((item) => item.startsWith('v1,')).map((item) => item.slice(3));
const expected = crypto.createHmac('sha256', key).update(message).digest();
return candidates.some((candidate) => {
const received = decodeBase64(candidate);
if (!received) return false;
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
});
}
Bayou publishes a test vector, which makes it easy to check the function before any real traffic arrives:
| Input | Value |
|---|---|
| Secret | whsec_ |
webhook-id | wh_12345 |
webhook-timestamp | 1750000000 |
| Body | {"event": "bills_ |
Expected webhook-signature | v1,yW3priuFZD+w+ji8R847LYfQxOKmZlBAHEt7OQP0CMI= |
Bayou also tells you to reject deliveries whose timestamp is more than five minutes from your clock, which is what the standardwebhooks library does by default. In a direct receiver that's reasonable. Behind Hookdeck it isn't: Hookdeck applies no timestamp tolerance, and a request it retries or replays keeps the original webhook-timestamp. The function above checks only the signature.
This receiver sends credential problems down a separate path from bill data, since one needs a person and the other needs a data pipeline:
const express = require('express');
// Returns the parsed body when it is a JSON object, otherwise null.
function parseJsonObject(rawBody) {
try {
const value = JSON.parse(rawBody.toString('utf8'));
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
} catch {
return null;
}
}
const CUSTOMER_EVENTS = new Set(['customer_has_filled_credentials', 'customer_must_reauthenticate']);
// queues.billing and queues.customers each expose add(job), which resolves once stored.
function createBayouReceiver(queues) {
const app = express();
app.post('/webhooks/bayou-energy', express.raw({ type: '*/*' }), async (req, res) => {
const secret = process.env.BAYOU_WEBHOOK_SECRET;
if (!secret) return res.sendStatus(500);
if (!verifySignature(req.body, req.headers, secret)) return res.sendStatus(401);
const body = parseJsonObject(req.body);
if (!body) return res.sendStatus(400); // signed, but not a JSON object
const { event, object } = body;
const job = { webhookId: req.headers['webhook-id'], event, object };
try {
await (CUSTOMER_EVENTS.has(event) ? queues.customers : queues.billing).add(job);
} catch {
return res.sendStatus(503);
}
return res.sendStatus(200);
});
return app;
}
A Hookdeck Bayou Energy source returns 200 to Bayou even for a request whose signature doesn't match, and flags it as failed verification in Hookdeck instead of delivering it.
Make Bayou Energy webhooks production-ready. Hookdeck Event Gateway checks the Standard Webhooks signature on every bill and customer event, stores each one, and lets you route bill data and credential alerts to different services.
Bayou Energy webhook limitations and pain points
One URL for everything
The Problem: Staging and production, or bill processing and customer support, all receive the same stream.
Why It Happens: A company has a single webhook URL and no per-event subscription.
Workarounds:
- Branch on
eventin one receiver and hand work to separate queues, as in the example above.
How Hookdeck Can Help: Hookdeck filters on the event field can send each event family to its own destination from one Bayou source.
Retries aren't documented
The Problem: You don't know whether a new_bill that hit a failing endpoint will be sent again.
Why It Happens: Bayou documents the events and signing but no retry policy, timeout or acknowledgement window.
Workarounds:
- Return
2xxas soon as the event is stored. - Reconcile periodically by listing a customer's bills through the API, so a missed webhook only delays data.
How Hookdeck Can Help: Hookdeck stores each event the moment Bayou sends it and retries delivery to your service, so a deploy or outage on your side doesn't depend on Bayou trying again.
Unparsed bills arrive before the data
The Problem: A bill shows up with no usable figures.
Why It Happens: new_unparsed_bill fires when Bayou finds a bill it couldn't parse yet. The parsed version arrives later as updated_bill.
Workarounds:
- Store the bill in a "processing" state and fill it in when
updated_billarrives for the same bill ID.
How Hookdeck Can Help: Hookdeck keeps both deliveries in its history, so you can check what each one contained when a bill's figures look wrong.
The five-minute rule and late deliveries
The Problem: Replayed or retried events fail your verifier.
Why It Happens: Bayou recommends rejecting timestamps older than five minutes, and the standardwebhooks library does this by default. Any later delivery of the same message keeps its original timestamp.
Workarounds:
- Check the signature without a window, as above, and deduplicate on
webhook-id.
How Hookdeck Can Help: Hookdeck doesn't apply a timestamp window when it verifies, so the age of a delivery never causes a rejection at the source.
Best practices
Wait for bills_ready and intervals_ready
These two events mark the end of the first pull for a customer. Fetch the full history after them, rather than assembling it from individual new_bill events during onboarding.
Send customers back quickly
When customer_ arrives, email or notify the customer with the onboarding_link. Bills stop updating until they share their credentials or MFA code again.
Deduplicate on webhook-id
There is no event ID in the body. Store the header value before acting. For the pattern, read how to implement webhook idempotency.
Keep bill links private
file_url points at the customer's bill document. Don't log full payloads, and restrict who can read stored events.
Conclusion
Bayou keeps the integration small, with one URL, seven events and a published test vector for the signature. Most of the effort goes into routing credential problems to people and bill data to your pipeline.
Hookdeck Event Gateway adds the delivery guarantees Bayou doesn't document, with stored events, retries and per-event routing. Get started for free.