# 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_has_filled_credentials` | 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_must_reauthenticate` | Customer | A customer must share their credentials again | Send the customer to `onboarding_link` |

`customer_must_reauthenticate` 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:

```json
{
  "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

1. Open your company's **Settings** page in the [Bayou dashboard](https://bayou.energy/dashboard).
2. Enter your endpoint in the **Webhook URL** field and save.
3. Copy the **Webhook signing secret** shown just below it, `whsec_` prefix included, and store it as `BAYOU_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_must_reauthenticate`.

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](/docs/cli) forwards that source to your local server:

```bash
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:

```javascript
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_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw` |
| `webhook-id` | `wh_12345` |
| `webhook-timestamp` | `1750000000` |
| Body | `{"event": "bills_ready", "object": {"id": 123456}}` |
| 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:

```javascript
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](/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 `event` in one receiver and hand work to separate queues, as in the example above.

**How Hookdeck Can Help:** Hookdeck [filters](/docs/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 `2xx` as 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](/docs/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_bill` arrives 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_must_reauthenticate` 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](/webhooks/guides/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](https://hookdeck.com) adds the delivery guarantees Bayou doesn't document, with stored events, retries and per-event routing. [Get started for free](https://dashboard.hookdeck.com/signup).