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
| Event | When |
|---|---|
balance.low | Account balance crosses your low-water threshold |
balance.depleted | Balance reached zero; workloads suspending |
workload.deployed | A workload finished initial deploy and is serving traffic |
workload.suspended | A workload was paused (manual or budget-driven) |
workload.crashed | A workload entered a crash loop |
workload.scaled | Scaling event: min/max changed, or scaled in/out |
energy_budget.warning | Workload at 80% of its daily energy budget |
energy_budget.exceeded | Workload hit 100%; suspending |
receipt.published | A signed receipt was issued (high-volume; opt in only) |
fleet.region_outage | A region you have workloads in is degraded |
compliance.evidence_ready | A 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
- Reply with any 2xx status to acknowledge.
- Any non-2xx (or no reply within 8 seconds) triggers retry with exponential backoff: 30s, 5m, 30m, 6h, 24h, 7d. After 7d we give up.
- Events are uniquely identified by
event_id; treat your handler as idempotent. - Order is best-effort, not guaranteed. If you need order, sort by
occurred_aton receipt.
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
- Balance auto-topup. Subscribe to
balance.low; have your handler call your finance system to authorize a top-up viaPOST /v1/billing/topup. - Crash → page. Subscribe to
workload.crashed; have your handler open a PagerDuty / Opsgenie incident. - Energy alert. Subscribe to
energy_budget.warning; have your handler post in Slack so you catch runaway loops before they exhaust the budget.