ProdBreak delivers real HTTP webhooks that fall out of real state transitions. The fixture stands up a local sink, registers it on your world, and exposes the signing secret and an inbox to assert against.

Receive and verify

const inbox = await sandbox.webhooks.sink();                 // local HTTP receiver
await sandbox.webhooks.register(inbox.url, { subscribe: ["task_run.completed"] });

// author the out-of-control payload value in the vocabulary you think in
await sandbox.exogenous.on("task_run.completed", "output.balance", 980.5);

const deck = sandbox.client();
const run = await deck.taskRuns.create({ taskId: "task_rent" });

await sandbox.clock.setDurations({ run_settle: "0s" });
await sandbox.clock.advance("0s");                           // drains the cascade, fires the webhook

const delivered = await inbox.waitFor("task_run.completed");
expect(delivered.verify(inbox.secret)).toBe(true);          // HMAC with the deterministic secret
expect(delivered.payload).toMatchObject({ output: { balance: 980.5 } });

Signing secrets are deterministic

Each endpoint’s secret is derived from (world key, endpoint url), so re-registering the same URL after a reset() yields the same secret — signature-verification tests are reproducible across runs. The fixture hands you inbox.secret; no copying values from a dashboard.

Webhook and GET always agree

The webhook payload and a later GET are two views of one frozen value (see set once, then frozen) — so this holds by construction:
const got = await deck.taskRuns.retrieve(run.id, { include: ["output"] });
expect(got.output?.balance).toBeCloseTo(980.5); // same value the webhook carried

Test your app’s handler, not just the inbox

To exercise your webhook handler instead of asserting on the sink, register your app’s endpoint URL instead of inbox.url:
await sandbox.webhooks.register("http://localhost:3000/webhooks/deck", {
  subscribe: ["task_run.completed"],
});
Under the MVP wall-clock mode, delivery happens via the scheduler in real time — collapsing durations to 0 makes it fire inline so waitFor resolves immediately. See Deterministic CI.