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

# Forward Live Webhooks to Your Local Machine with Repost

> Use repost forward to connect an internal forwarder to localhost, receive real webhook events in the CLI TUI, and inspect request and response details.

export const CliFlow = ({caption, frame = true}) => {
  const W = 960;
  const H = 360;
  const CY = 180;
  const u = n => `${(n / W * 100).toFixed(3)}cqw`;
  const MONO = "ui-monospace, monospace";
  const uid = "cli";
  const WEBHOOK = ["M18 16.98h-5.99c-1.1 0-1.95.94-2.48 1.9A4 4 0 0 1 2 17c.01-.7.2-1.4.57-2", "m6 17 3.13-5.78c.53-.97.1-2.18-.5-3.1a4 4 0 1 1 6.89-4.06", "m12 6 3.13 5.73C15.66 12.7 16.9 13 18 13a4 4 0 0 1 0 8"];
  const SRC = {
    cx: 140,
    w: 124,
    edge: 202
  };
  const BUCKET = {
    cx: 360,
    w: 118,
    h: 150
  };
  const FWD = {
    cx: 600,
    w: 122,
    h: 152
  };
  const CLI = {
    cx: 830,
    w: 196,
    h: 170
  };
  const sources = [{
    label: "Stripe",
    swatch: "#635BFF"
  }, {
    label: "Shopify",
    swatch: "#95BF47"
  }, {
    label: "GitHub"
  }];
  const sCy = [80, 180, 280];
  const ports = [150, 180, 210];
  const wirePath = (x1, y1, x2, y2) => {
    const dx = (x2 - x1) * 0.5;
    return `M ${x1} ${y1} C ${x1 + dx} ${y1}, ${x2 - dx} ${y2}, ${x2} ${y2}`;
  };
  const placed = (cx, cy, w) => ({
    position: "absolute",
    left: `${cx / W * 100}%`,
    top: `${cy / H * 100}%`,
    width: `${w / W * 100}%`,
    transform: "translate(-50%, -50%)"
  });
  const label = (cx, title, sub) => <div style={{
    position: "absolute",
    left: `${cx / W * 100}%`,
    top: `${286 / H * 100}%`,
    width: `${220 / W * 100}%`,
    transform: "translateX(-50%)",
    textAlign: "center",
    display: "flex",
    flexDirection: "column",
    gap: u(4)
  }}>
      <span style={{
    fontSize: u(19),
    fontWeight: 700,
    color: "var(--fd-text)"
  }}>{title}</span>
      <span style={{
    fontFamily: MONO,
    fontSize: u(13),
    color: "var(--fd-muted)"
  }}>{sub}</span>
    </div>;
  const NODE = {
    boxSizing: "border-box",
    display: "flex",
    alignItems: "center",
    justifyContent: "center",
    gap: u(11),
    padding: `${u(12)} ${u(18)}`,
    fontSize: u(19),
    fontWeight: 600,
    lineHeight: 1,
    whiteSpace: "nowrap",
    color: "var(--fd-text)",
    background: "var(--fd-node-bg)",
    border: "1px solid var(--fd-node-border)",
    backdropFilter: "blur(var(--fd-blur))",
    WebkitBackdropFilter: "blur(var(--fd-blur))"
  };
  const bucket = <div style={{
    ...placed(BUCKET.cx, CY, BUCKET.w),
    height: `${BUCKET.h / H * 100}%`,
    boxSizing: "border-box",
    display: "flex",
    alignItems: "center",
    justifyContent: "center",
    overflow: "hidden",
    background: "var(--fd-bucket-bg)",
    border: "1px solid var(--fd-bucket-border)"
  }}>
      <span aria-hidden="true" className="fd-shine-border" style={{
    backgroundImage: "radial-gradient(transparent, transparent, var(--fd-shine), transparent, transparent)",
    "--fd-border-width": "var(--fd-shine-width)",
    "--fd-duration": "var(--fd-shine-duration)"
  }} />
      <svg viewBox="0 0 24 24" fill="none" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" style={{
    width: u(34),
    height: u(34),
    stroke: "var(--fd-bucket-icon)"
  }}>
        {WEBHOOK.map((d, i) => <path key={i} d={d} />)}
      </svg>
    </div>;
  const forwarder = <div style={{
    ...placed(FWD.cx, CY, FWD.w),
    height: `${FWD.h / H * 100}%`,
    boxSizing: "border-box",
    display: "flex",
    flexDirection: "column",
    justifyContent: "center",
    gap: u(16),
    padding: `${u(22)} ${u(22)}`,
    background: "var(--fd-node-bg)",
    border: "1px solid var(--fd-node-border)",
    backdropFilter: "blur(var(--fd-blur))",
    WebkitBackdropFilter: "blur(var(--fd-blur))"
  }}>
      <span style={{
    height: u(13),
    width: "70%",
    background: "var(--fd-accent)"
  }} />
      <span style={{
    height: u(13),
    width: "100%",
    background: "var(--fd-wire)"
  }} />
      <span style={{
    height: u(13),
    width: "100%",
    background: "var(--fd-wire)"
  }} />
      <span className="fd-pulse" style={{
    marginTop: u(4),
    height: u(15),
    width: u(15),
    borderRadius: "9999px",
    background: "var(--fd-accent-cool)"
  }} />
    </div>;
  const bars = [{
    w: "62%",
    c: "var(--fd-accent)"
  }, {
    w: "44%",
    c: "var(--fd-ok)"
  }, {
    w: "72%",
    c: "var(--fd-accent-cool)"
  }, {
    w: "38%",
    c: "rgba(255,255,255,0.18)"
  }];
  const terminal = <div style={{
    ...placed(CLI.cx, CY, CLI.w),
    height: `${CLI.h / H * 100}%`,
    boxSizing: "border-box",
    display: "flex",
    flexDirection: "column",
    gap: u(15),
    padding: `${u(22)} ${u(22)}`,
    background: "#0d0e12",
    border: "1px solid rgba(255,255,255,0.10)"
  }}>
      <div style={{
    display: "flex",
    alignItems: "center",
    fontFamily: MONO,
    fontSize: u(16),
    lineHeight: 1
  }}>
        <span style={{
    color: "#71717a"
  }}>$ </span>
        <span style={{
    color: "#e5e7eb"
  }}>repost forward</span>
        <span className="fd-blink" style={{
    marginLeft: u(2),
    width: u(2.5),
    height: u(18),
    background: "var(--fd-accent)"
  }} />
      </div>
      {bars.map((b, i) => <span key={i} style={{
    height: u(13),
    width: b.w,
    background: b.c
  }} />)}
    </div>;
  const solidWires = sources.map((_, i) => wirePath(SRC.edge, sCy[i], BUCKET.cx - BUCKET.w / 2, ports[i]));
  const legBucketFwd = `M ${BUCKET.cx + BUCKET.w / 2} ${CY} L ${FWD.cx - FWD.w / 2} ${CY}`;
  const legFwdCli = `M ${FWD.cx + FWD.w / 2} ${CY} L ${CLI.cx - CLI.w / 2} ${CY}`;
  const dashedLegs = [legBucketFwd, legFwdCli];
  const stage = <div style={{
    position: "relative",
    width: "100%",
    aspectRatio: "960 / 360",
    containerType: "inline-size"
  }}>
      <svg viewBox="0 0 960 360" preserveAspectRatio="none" style={{
    position: "absolute",
    inset: 0,
    width: "100%",
    height: "100%"
  }} aria-hidden="true">
        <defs>
          <pattern id={`${uid}-grid`} width="28" height="28" patternUnits="userSpaceOnUse">
            <rect width="1.6" height="1.6" style={{
    fill: "var(--fd-grid)"
  }} />
          </pattern>
        </defs>
        <rect x="0" y="0" width="960" height="360" fill={`url(#${uid}-grid)`} opacity="0.6" />

        {}
        {solidWires.map((d, i) => <path key={`sw${i}`} id={`${uid}-sw-${i}`} d={d} fill="none" strokeWidth="2" style={{
    stroke: "var(--fd-wire)"
  }} />)}
        {solidWires.map((_, i) => <rect key={`sp${i}`} x="-4" y="-4" width="8" height="8" style={{
    fill: "var(--fd-accent)"
  }}>
            <animateMotion dur="2.6s" begin={`${i * 0.35}s`} repeatCount="indefinite">
              <mpath href={`#${uid}-sw-${i}`} />
            </animateMotion>
          </rect>)}

        {}
        {dashedLegs.map((d, i) => <path key={`dl${i}`} id={`${uid}-dl-${i}`} d={d} fill="none" strokeWidth="2.5" strokeDasharray="14 14" style={{
    stroke: "var(--fd-wire)"
  }}>
            <animate attributeName="stroke-dashoffset" from="28" to="0" dur="0.9s" repeatCount="indefinite" />
          </path>)}
        {dashedLegs.map((_, i) => <rect key={`dp${i}`} x="-4.5" y="-4.5" width="9" height="9" style={{
    fill: "var(--fd-accent-cool)"
  }}>
            <animateMotion dur="1.8s" begin={`${0.6 + i * 0.5}s`} repeatCount="indefinite">
              <mpath href={`#${uid}-dl-${i}`} />
            </animateMotion>
          </rect>)}
      </svg>

      {sources.map((s, i) => <div key={`s${i}`} style={{
    ...placed(SRC.cx, sCy[i], SRC.w),
    ...NODE
  }}>
          <span style={{
    width: u(15),
    height: u(15),
    flex: "0 0 auto",
    background: s.swatch || "var(--fd-swatch-neutral)"
  }} />
          <span>{s.label}</span>
        </div>)}

      {bucket}
      {forwarder}
      {terminal}

      {label(BUCKET.cx, "Bucket", "Webhook ingress")}
      {label(FWD.cx, "Internal forwarder", "Waits for CLI listener")}
      {label(CLI.cx, "CLI to localhost", "Inspect, copy, generate types")}
    </div>;
  return <div className="not-prose" style={{
    width: "100%"
  }}>
      {caption && <div style={{
    display: "flex",
    alignItems: "center",
    gap: "8px",
    marginBottom: "12px",
    fontSize: "11px",
    fontWeight: 600,
    letterSpacing: "0.04em",
    textTransform: "uppercase",
    color: "var(--fd-muted)"
  }}>
          <span className="fd-pulse" style={{
    width: "8px",
    height: "8px",
    background: "var(--fd-accent)"
  }} />
          {caption}
        </div>}
      {frame ? <div style={{
    padding: "16px",
    background: "var(--fd-surface)",
    border: "1px solid var(--fd-surface-border)"
  }}>{stage}</div> : stage}
    </div>;
};

Local forwarding is how the Repost CLI delivers real webhook traffic to a service running on your machine. When you run `repost forward`, the CLI attaches to one or more internal forwarders, streams incoming events to your local HTTP endpoint, and shows request and response details in an interactive terminal UI.

<CliFlow />

## Prerequisites

Before starting a forwarding session, make sure you have the following in place:

<CardGroup cols={3}>
  <Card title="CLI installed and authenticated" icon="terminal">
    Install the binary and run `repost auth login` or set `REPOST_TOKEN`. See [Authentication](/docs/cli/authentication) if you haven't done this yet.
  </Card>

  <Card title="An internal forwarder" icon="route">
    `repost forward` only attaches to internal forwarders. Create one on a queued bucket in the Repost dashboard before running the command.
  </Card>

  <Card title="A running local endpoint" icon="laptop">
    Your application must be running and listening at the target URL configured on the internal forwarder.
  </Card>
</CardGroup>

## Start a forwarding session

`repost forward` resolves a bucket first, then resolves internal forwarders inside that bucket.

<Tabs>
  <Tab title="Interactive">
    Run with no flags. The CLI prompts you to select a bucket from your account, then prompts you to pick one or more internal forwarders from that bucket.

    ```bash theme={"languages":{"custom":["/languages/repost.json"]}}
    repost forward
    ```
  </Tab>

  <Tab title="Scripted">
    Pass `--bucket` and `--forwarder` to skip the prompts entirely. This is useful in scripts and repeated workflows.

    ```bash theme={"languages":{"custom":["/languages/repost.json"]}}
    repost forward \
      --bucket stripe-prod \
      --forwarder local-webhooks
    ```
  </Tab>

  <Tab title="Multiple forwarders">
    Repeat `--forwarder` (or `-f`) to attach to more than one internal forwarder in the same bucket at the same time.

    ```bash theme={"languages":{"custom":["/languages/repost.json"]}}
    repost forward \
      -b stripe-prod \
      -f local-api \
      -f local-worker
    ```
  </Tab>
</Tabs>

<Warning>
  If you provide `--bucket` or `--forwarder`, provide both. Partial flag usage is rejected so the CLI never connects to an ambiguous target.
</Warning>

<Note>
  `--bucket` expects a bucket slug for local forwarding. `--bucket-slug` remains accepted as a compatibility alias. `--forwarder` expects an internal forwarder name. External forwarders are filtered out because they are delivered by Repost.
</Note>

## Handle pending events

When a forwarder has events queued up from before your session started, the CLI can ask what to do, drain the queue first, or skip it entirely and jump straight to new traffic.

<Tabs>
  <Tab title="Prompt (default)">
    Without any pending-event flag, the TUI asks per forwarder whether to receive or skip pending events before starting real-time delivery.

    ```bash theme={"languages":{"custom":["/languages/repost.json"]}}
    repost forward -b stripe-prod -f local-webhooks
    ```
  </Tab>

  <Tab title="Pending first">
    Drain all queued events first, then seamlessly continue with new events as they arrive.

    ```bash theme={"languages":{"custom":["/languages/repost.json"]}}
    repost forward \
      -b stripe-prod \
      -f local-webhooks \
      --pending-first
    ```
  </Tab>

  <Tab title="Skip pending">
    Mark all queued events as skipped for this session and receive only new events going forward.

    ```bash theme={"languages":{"custom":["/languages/repost.json"]}}
    repost forward \
      -b stripe-prod \
      -f local-webhooks \
      --skip-pending
    ```
  </Tab>
</Tabs>

<Warning>
  Do not combine `--pending-first` and `--skip-pending`. If both flags are present, `--skip-pending` takes precedence and queued events are skipped.
</Warning>

## Use the TUI

The terminal UI shows a live stream of forwarded events. Each row displays the event status, response code, target, and duration. Use the following shortcuts to navigate and inspect traffic.

### Event list

| Key                     | Action                                              |
| ----------------------- | --------------------------------------------------- |
| `j` / `k` or arrow keys | Move selection up and down.                         |
| `g` / `Home`            | Jump to the top of the list.                        |
| `G` / `End`             | Jump to the bottom of the list.                     |
| `Space`                 | Open the detail panel for the selected event.       |
| `c`                     | Copy the selected row context for debugging.        |
| `t`                     | Generate types from the selected JSON request body. |
| `Ctrl+L`                | Clear all local rows from the view.                 |

### Detail panel

Press `Space` on any event to open its detail panel. The panel shows request headers, request body, response headers, response body, status code, target URL, and duration.

| Key              | Action                                |
| ---------------- | ------------------------------------- |
| `Space` or `Esc` | Close the detail panel.               |
| `c`              | Copy the detail view.                 |
| `t`              | Generate types from the request body. |

### Generate types panel

Press `t` on any event with a JSON body to open the type generation panel. You can switch language, rename the root type, and toggle inference options without leaving the terminal.

| Key                     | Action                                                      |
| ----------------------- | ----------------------------------------------------------- |
| `h` / `l` or arrow keys | Switch between supported languages.                         |
| `Tab`                   | Edit the root type name.                                    |
| `o`                     | Open the options panel.                                     |
| `Space` / `Enter`       | Toggle an option, cycle a value, or commit an edited value. |
| `c`                     | Copy the generated code.                                    |
| `Esc`                   | Close the generate types panel.                             |

### Quit

Press `q` or `Ctrl+C` from any view to end the forwarding session and exit the CLI.

## Run in headless mode

Use `--headless` when you want `repost forward` to run without the terminal UI, for example in CI pipelines or test scripts. In headless mode, the CLI prints `HEADLESS_READY` to stdout after the connection is fully set up, which you can use as a signal in scripts.

```bash theme={"languages":{"custom":["/languages/repost.json"]}}
repost forward \
  -b stripe-prod \
  -f local-webhooks \
  --headless \
  --skip-pending
```

<Tip>
  Combine `--headless` with `--skip-pending` in automation so the CLI does not pause to ask about pending events. The process starts forwarding new events immediately after printing `HEADLESS_READY`.
</Tip>

## Internal forwarder state

An internal forwarder starts in a paused state until the CLI connects to it. Once you run `repost forward`, the forwarder becomes active and begins delivering events. If your CLI session disconnects, the forwarder returns to a paused state and holds new events until you reconnect.

Reconnecting is as simple as running `repost forward` again with the same flags. The Repost dashboard shows **No listeners** on the forwarder card when no CLI session is currently connected.

## Common fixes

<AccordionGroup>
  <Accordion title="The CLI says I am not authenticated">
    Run `repost auth login` to sign in through the browser, or set `REPOST_TOKEN` to an API token from your Repost dashboard. See [Authentication](/docs/cli/authentication) for details.
  </Accordion>

  <Accordion title="The bucket slug is not found">
    Run `repost bucket list` to see the exact slug for each bucket in your account. The `--bucket` flag expects the slug value from the `SLUG` column, not the bucket ID or display name.
  </Accordion>

  <Accordion title="No internal forwarders appear">
    Only internal forwarders are available to `repost forward`. If you only have external forwarders on a bucket, create an internal forwarder in the Repost dashboard first.
  </Accordion>

  <Accordion title="Another session took over">
    Only one active CLI session can own a given forwarder at a time. Close the other session and run `repost forward` again from the terminal you want to use.
  </Accordion>
</AccordionGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="Reference" icon="list-tree" href="/docs/cli/reference">
    See all commands, flags, TUI shortcuts, and environment variables in the complete CLI reference.
  </Card>

  <Card title="Authentication" icon="key-round" href="/docs/cli/authentication">
    Review authentication methods and credential priority if you are setting up the CLI for CI or a new machine.
  </Card>
</CardGroup>
