Webhooks

Joule Cloud emits webhooks for account events you might want to react to programmatically — balance thresholds, energy budget warnings, workload lifecycle, receipt published. All delivered over HTTPS POST to an endpoint you control; all signed with HMAC-SHA256 so you can verify origin without making outbound calls back to us.

Configure

In the portal: Settings → Webhooks → New endpoint. Or via CLI:

jc webhook create \
  --url https://your.app/_jc/webhook \
  --events balance.low,workload.crashed,energy_budget.warning

The CLI prints a signing secret (starts whsec_…); keep it private. You can rotate at any time with jc webhook rotate.

Event types

EventWhen
balance.lowAccount balance crosses your low-water threshold
balance.depletedBalance reached zero; workloads suspending
workload.deployedA workload finished initial deploy and is serving traffic
workload.suspendedA workload was paused (manual or budget-driven)
workload.crashedA workload entered a crash loop
workload.scaledScaling event: min/max changed, or scaled in/out
energy_budget.warningWorkload at 80% of its daily energy budget
energy_budget.exceededWorkload hit 100%; suspending
receipt.publishedA signed receipt was issued (high-volume; opt in only)
fleet.region_outageA region you have workloads in is degraded
compliance.evidence_readyA compliance evidence pack you requested is ready to download

Delivery

POST with a JSON body. Standard headers:

POST /your-endpoint
Host: your.app
Content-Type: application/json
User-Agent: JouleCloud-Webhooks/1.0
X-JC-Event: workload.crashed
X-JC-Event-Id: evt_018a3c…
X-JC-Timestamp: 1782219834
X-JC-Signature: t=1782219834,v1=9c6563f6a7d4…

{
  "event": "workload.crashed",
  "event_id": "evt_018a3c...",
  "occurred_at": "2026-06-23T13:43:54Z",
  "data": {
    "workload_id": "wl_xxx",
    "workload_name": "api",
    "exit_code": 137,
    "last_log_url": "https://api.greenjoules.cloud/v1/workloads/wl_xxx/logs?lines=200"
  }
}

Verifying signatures

The X-JC-Signature header is a comma-separated t= timestamp + one or more v1= HMAC-SHA256 values (multiple signatures may appear during secret rotation). Verify both:

# Python
import hmac, hashlib

def verify(payload_bytes, header, secret):
    parts = dict(p.split("=", 1) for p in header.split(","))
    ts = parts["t"]
    msg = (ts + "." + payload_bytes.decode()).encode()
    expected = hmac.new(secret.encode(), msg, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])

# Reject if the timestamp is too old (replay defence)
import time
if abs(time.time() - int(ts)) > 300:
    return 400
// Node
import crypto from "node:crypto";
import express from "express";
const app = express();
app.post("/_jc/webhook", express.raw({ type: "*/*" }), (req, res) => {
  const header = req.header("X-JC-Signature");
  const parts = Object.fromEntries(header.split(",").map(p => p.split("=")));
  const expected = crypto.createHmac("sha256", process.env.JC_WHSEC)
    .update(parts.t + "." + req.body.toString())
    .digest("hex");
  if (expected !== parts.v1) return res.sendStatus(401);
  res.sendStatus(200);
});

Retries

Testing

# send a synthetic event to your endpoint
jc webhook test --endpoint <id> --event workload.crashed
# tail what you sent
jc webhook log --since 1h

Disable temporarily

jc webhook pause   <id>
jc webhook resume  <id>
jc webhook delete  <id>

Patterns