> ## Documentation Index
> Fetch the complete documentation index at: https://docs.prodbreak.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Test a webhook

> Receive a real signed webhook, verify it, and confirm it agrees with the GET.

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

<Tabs>
  <Tab title="Jest / Vitest">
    ```ts theme={null}
    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 } });
    ```
  </Tab>

  <Tab title="pytest">
    ```python theme={null}
    inbox = deck_sandbox.webhooks.sink()
    deck_sandbox.webhooks.register(inbox.url, subscribe=["task_run.completed"])
    deck_sandbox.exogenous.on("task_run.completed", "output.balance", 980.5)

    deck = deck_sandbox.client()
    run = deck.task_runs.create(task_id="task_rent")

    deck_sandbox.clock.set_durations(run_settle="0s")
    deck_sandbox.clock.advance("0s")

    delivered = inbox.wait_for("task_run.completed")
    assert delivered.verify(inbox.secret)
    assert delivered.payload["output"]["balance"] == 980.5
    ```
  </Tab>
</Tabs>

## 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](/concepts/exogenous#set-once-then-frozen)) — so this holds by
construction:

```ts theme={null}
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`:

```ts theme={null}
await sandbox.webhooks.register("http://localhost:3000/webhooks/deck", {
  subscribe: ["task_run.completed"],
});
```

<Note>
  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](/guides/deterministic-ci).
</Note>
