Guides
Webhooks
If you would rather Octopus called you than hold a socket open, register an endpoint. Same changes, delivered as a signed HTTPS POST.
Delivery and verification
Steps 2 and 3 explain the header's shape. The timestamp is signed together with the body, not alongside it. A signature over the body alone would let anyone who captured one real POST replay it forever; because the time is inside the signed string, a replay carries an old t that you reject at step 5.
Step 7 must be a constant-time comparison. A plain === on hex strings leaks, through timing, how many leading characters were right, which is enough to forge a signature given patience.
Headers on every delivery
x-octopus-signature: t=1789125548,v1=9f2c8ab1...
x-octopus-delivery: b754d5dc-f1a5-45cc-9220-7571e992dd1f
x-octopus-event: TASK_UPDATEDThe scheme is versioned, v1 sits in the header so a v2 can be added later without breaking every receiver at once.
Verifying
import crypto from "node:crypto";
export function verify(rawBody, header, secret) {
const parts = Object.fromEntries(
header.split(",").map((p) => p.split("="))
);
const t = Number(parts.t);
// Two-sided window: rejects replays AND clocks in the future.
if (Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${t}.${rawBody}`) // the RAW body, byte for byte
.digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(parts.v1, "hex");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Sign the raw body, not the parsed one
Verify before your JSON middleware touches the request. Parsing and re-serialising changes key order and whitespace, and the signature will never match. This is the single most common reason a correct-looking implementation still fails.
Answer fast, work later
Return 2xx as soon as you have verified and queued the work, then process out of the request. A slow endpoint looks like a failing one and will be retried, deduplicate on x-octopus-delivery, which is stable across retries.
Proving an endpoint before you rely on it
Settings → Integrations has a ping that sends a synthetic event, and a delivery log showing recent attempts with the response each one got. That log is the only way to debug a receiver that is silently refusing.
HTTPS only, and no internal addresses
Octopus resolves your hostname before every delivery and refuses private and loopback addresses. A webhook pointed at an internal service would otherwise turn the dispatcher into a way to reach inside our network.
