Skip to content

Run and integrate

Webhooks

Events, payloads, signature checks in Python and Node, retries and auto-disable.

On this page

A webhook sends an HTTPS POST to your endpoint when something happens in your org, so a pipeline can pick up a finished run without polling. Webhooks come with Pro and Team, and they're set per org under Settings > Webhooks, not per run. Endpoints must use https and resolve to a public address; private and internal addresses are refused.

Events#

EventWhen
run.succeededA hosted run finished. Its checks may still have failed: read checks_passed and checks_total.
run.failedA hosted run ended with an error.
spec.needs_reviewA spec version compiled and waits for an admin to approve it.
webhook.testYou pressed Send test event.

The request#

HTTP request
POST /your/path HTTP/1.1
Content-Type: application/json
User-Agent: DOGM-Webhooks/1.0
DOGM-Event: run.succeeded
DOGM-Delivery: 0b9f3c1e-6d0a-4c43-9a51-4a3e7f2b8c10
DOGM-Signature: t=1793318400,v1=5d1f0c...

{"id":"run.succeeded:7c2e...","type":"run.succeeded","created_at":"2026-10-30T08:00:00+00:00","data":{"run":{...}}}
  • id is the same on every attempt of one event. Store it and skip events you've already handled, because retries mean you can receive one twice.
  • data.run has the run's id, project_id, spec_id, spec_version_id, spec_version, spec_hash, status, seed (a decimal string, since a 64-bit seed doesn't fit a JSON number safely), format, output_sha256, checks_passed, checks_total, error ({ code, summary } or null), created_at and finished_at.
  • data.spec and data.version describe a spec.needs_review event.

Answer with any 2xx within 10 seconds. Redirects aren't followed, and the response body is ignored.

Verify the signature#

DOGM-Signature holds a Unix timestamp t and v1, the hex HMAC-SHA256 of {t}.{body} keyed with your endpoint's signing secret (whsec_..., shown once when you create the endpoint). Verify against the raw request body, before you parse the JSON, and reject timestamps more than 5 minutes old so a captured request can't be replayed later.

Python#

Python
import hashlib
import hmac
import time


def verify_dogm_signature(secret: str, header: str, body: bytes, tolerance: int = 300) -> bool:
    """secret: your whsec_... secret; header: the DOGM-Signature value; body: the raw request body."""
    try:
        parts = dict(item.split("=", 1) for item in header.split(","))
        timestamp = int(parts["t"])
        signature = parts["v1"]
    except (KeyError, ValueError):
        return False
    if abs(time.time() - timestamp) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{timestamp}.".encode() + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)

With FastAPI:

Python
from fastapi import FastAPI, HTTPException, Request

app = FastAPI()


@app.post("/dogm")
async def dogm_webhook(request: Request) -> dict:
    body = await request.body()
    if not verify_dogm_signature(DOGM_WEBHOOK_SECRET, request.headers.get("DOGM-Signature", ""), body):
        raise HTTPException(400, "bad signature")
    event = await request.json()
    # event["id"] is stable across retries: skip it if you have handled it before.
    return {"ok": True}

Node.js#

JavaScript
import { createHmac, timingSafeEqual } from "node:crypto";

// secret: your whsec_... secret; header: the DOGM-Signature value; body: the raw request body (a Buffer).
export function verifyDogmSignature(secret, header, body, toleranceSeconds = 300) {
  const parts = Object.fromEntries(
    (header ?? "").split(",").map((item) => {
      const i = item.indexOf("=");
      return [item.slice(0, i), item.slice(i + 1)];
    }),
  );
  const timestamp = Number.parseInt(parts.t, 10);
  if (!Number.isFinite(timestamp) || !parts.v1) return false;
  if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;
  const expected = createHmac("sha256", secret).update(`${timestamp}.`).update(body).digest();
  const given = Buffer.from(parts.v1, "hex");
  return given.length === expected.length && timingSafeEqual(given, expected);
}

With Express, read the raw body for this route, or the bytes you verify won't be the bytes DOGM signed:

JavaScript
import express from "express";

const app = express();

app.post("/dogm", express.raw({ type: "application/json" }), (req, res) => {
  if (!verifyDogmSignature(process.env.DOGM_WEBHOOK_SECRET, req.get("DOGM-Signature"), req.body)) {
    return res.status(400).send("bad signature");
  }
  const event = JSON.parse(req.body);
  // event.id is stable across retries: skip it if you have handled it before.
  res.json({ ok: true });
});

Retries#

A delivery that doesn't get a 2xx is retried 20 times over about 24 hours: after 30 seconds, then 1, 2, 5, 10, 15 and 30 minutes, and so on up to 3.5 hours apart. Each attempt is signed again with a new timestamp. From Settings > Webhooks you can see recent deliveries with their status and response code, retry a failed one, and send a test event.

Auto-disable#

When every delivery attempt to an endpoint has failed for 24 hours, DOGM disables the endpoint and emails the org's owners and admins. Events that happen while it's disabled aren't recorded or sent, so nothing replays when you turn it back on. Fix the endpoint, then re-enable it from Settings > Webhooks.