Guides
Webhooks
A webhook endpoint receives the events a workspace's alert rules route to it: a blocked release, a result waiting on a person, a run that ended, a monitor's alarm. Each delivery is a JSON body signed with the endpoint's secret, so your server can check it came from your workspace before acting on it.
Add an endpoint
On the workspace's Integrations page, an Owner or Admin adds a signed webhook with a name and a public https URL on port 443 or 8443. The page then shows the endpoint's signing secret, which starts with whsec_. Copy it then: it is stored encrypted and is not shown again. To replace it, delete the endpoint and add it again.
Then, on the Alerts page, choose the endpoint as a channel of one or more rules. "Send a test" posts an oloproof.test delivery and shows the status your server answered.
What is posted
{
"type": "oloproof.gate_blocked",
"id": "dlv_8c1f0a2b3c4d5e6f7a8b9c0d",
"created_at": "2026-10-26T14:03:00.000Z",
"workspace": "acme",
"rule": { "id": "rul_1a2b3c4d5e6f7a8b9c0d1e2f", "name": "Do not ship" },
"event": "gate_blocked",
"items": [
{
"project": "support",
"kind": "run",
"id": "run_20261026T140100_4f2a",
"release_action": "BLOCK",
"rules": [["faithfulness", "FAIL"]],
"href": "https://oloproof.com/app/workspaces/acme/projects/support/runs/run_20261026T140100_4f2a"
}
],
"more": 0
}An item carries ids, the engine's decision states, the release action, a job's state and outcome, a monitor's level, threshold and interval as the engine wrote them, and links. It never carries a case's input, output, expected answer or labels. A delivery holds at most 50 items; more counts the events past them.
Verify a delivery
Every delivery carries three headers, in the Standard Webhooks form:
- webhook-id: the delivery's id, the same on every retry.
- webhook-timestamp: when this attempt was sent, in seconds since 1970.
- webhook-signature: v1, and the base64 HMAC-SHA256 of id.timestamp.body, keyed by the secret's bytes (the part after whsec_, base64-decoded).
Compute the signature over the raw body as received, compare it in constant time, and refuse a timestamp more than five minutes from your clock.
import base64, hashlib, hmac, time
def verify(secret: str, headers: dict, body: bytes) -> bool:
key = base64.b64decode(secret.removeprefix("whsec_"))
msg = f"{headers['webhook-id']}.{headers['webhook-timestamp']}.".encode() + body
expected = base64.b64encode(hmac.new(key, msg, hashlib.sha256).digest()).decode()
fresh = abs(time.time() - int(headers["webhook-timestamp"])) <= 300
given = [part.split(",", 1)[-1] for part in headers["webhook-signature"].split()]
return fresh and any(hmac.compare_digest(expected, sig) for sig in given)import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(secret, headers, body) {
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const id = headers["webhook-id"], ts = headers["webhook-timestamp"];
const expected = createHmac("sha256", key).update(`${id}.${ts}.${body}`).digest("base64");
const fresh = Math.abs(Date.now() / 1000 - Number(ts)) <= 300;
return fresh && headers["webhook-signature"].split(" ").some((part) => {
const sig = Buffer.from(part.split(",").pop());
return sig.length === expected.length && timingSafeEqual(sig, Buffer.from(expected));
});
}The Standard Webhooks libraries verify the same headers.
Delivery
Answer with any 2xx status within ten seconds. Anything else, a redirect included, is a failed attempt, retried after 1, 5, 30, 120 and 360 minutes and then given up. Because an answer can be lost after your server acted on it, the same delivery can arrive twice: use webhook-id to drop a repeat. A rule fires at most once for each event, so a new id is a new delivery.
Before each attempt the endpoint's name is resolved again, and the request goes only to an address that is public: loopback, private, link-local, shared, reserved and cloud metadata addresses are refused, as are IPv6 forms that carry one of them. The connection is made to the address that was checked, and the certificate is checked against the name.
Slack
A Slack endpoint is an incoming-webhook URL. It receives a short English message, one line per event, with a link to each result. Slack does not check the signature headers, which are sent anyway.