Webhooks & signatures
Blue Oceans posts JSON events to your HTTPS endpoint. Each delivery is signed with your endpoint secret so you can prove it came from us and has not been altered or replayed.
Signature scheme
| Header | BO-Signature: t=<unix seconds>,v1=<hex> |
|---|---|
| Algorithm | HMAC-SHA256 over the string `${t}.${rawBody}` using your endpoint secret |
| Replay window | Reject if the timestamp is more than 300 seconds from your clock |
| Comparison | Constant-time (timingSafeEqual / hmac.compare_digest) |
| De-duplication | BO-Event-Id header — store processed IDs; deliveries are at-least-once |
| Retries | Non-2xx or timeout (10 s) → exponential backoff (1 min, 5 min, 30 min, 2 h, 6 h …) for up to 24 hours, then the endpoint is flagged in your admin console |
| Secrets | Shown once on creation, rotatable with a 24-hour overlap; stored encrypted, only the last four characters are displayed |
Verify in Node.js
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyBlueOceans(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(header.split(",").map((p) => p.trim().split("=")));
const t = Number(parts.t);
if (!t || !parts.v1) return false;
if (Math.abs(Date.now() / 1000 - t) > toleranceSec) return false; // replay window
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
const given = Buffer.from(parts.v1, "hex");
return given.length === expected.length && timingSafeEqual(given, expected);
}
// Express: use the RAW body, not re-serialised JSON
app.post("/hooks/blue-oceans", express.raw({ type: "application/json" }), (req, res) => {
if (!verifyBlueOceans(req.body.toString("utf8"), req.get("BO-Signature") ?? "", process.env.BO_WEBHOOK_SECRET)) {
return res.sendStatus(400);
}
const eventId = req.get("BO-Event-Id"); // de-duplicate on this
queue.add(eventId, JSON.parse(req.body)); // acknowledge fast, process async
res.sendStatus(204);
});Verify in Python
import hmac, hashlib, time
def verify_blue_oceans(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
parts = dict(p.strip().split("=", 1) for p in header.split(",") if "=" in p)
try:
t = int(parts["t"])
except (KeyError, ValueError):
return False
if abs(time.time() - t) > tolerance:
return False # replay window
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))Try it
Try editing one character of the body after signing — verification fails with mismatch. Signatures older than five minutes fail with expired. Never paste production secrets here.
Event catalogue
deliverable.acceptedA deliverable passed evidence-gated acceptance by a second person.
{
"id": "evt_01",
"type": "deliverable.accepted",
"tenant": "kusasa",
"occurredAt": "2026-10-20T07:58:00Z",
"data": {
"deliverableId": "DLV-1042",
"acceptedBy": "w-02",
"evidenceCount": 2
}
}tenant.activatedA new workspace finished the 24-hour activation and is workspace-ready.
{
"id": "evt_02",
"type": "tenant.activated",
"tenant": "sample-courier",
"occurredAt": "2026-10-20T07:31:00Z",
"data": {
"plan": "growth",
"modules": [
"serviceos",
"peopleos"
],
"hoursToActivate": 6.4
}
}invoice.issuedA subscription or implementation invoice was issued (billing switches are OFF in the demo).
{
"id": "evt_03",
"type": "invoice.issued",
"tenant": "kusasa",
"occurredAt": "2026-10-01T00:05:00Z",
"data": {
"invoiceId": "INV-S-0042",
"amountExclVat": 3999,
"currency": "ZAR"
}
}obligation.dueA compliance obligation reached a 90/60/30-day alert threshold.
{
"id": "evt_04",
"type": "obligation.due",
"tenant": "kusasa",
"occurredAt": "2026-10-20T06:00:00Z",
"data": {
"obligationId": "OB-03",
"daysRemaining": 8
}
}Machine-readable list: /api/v1/webhooks/events.