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

# Provision buckets and forwarders

> Create the route a webhook travels — a bucket that receives and a forwarder that delivers — idempotently, and verify the topology.

A webhook route has two halves: a **bucket** receives inbound requests at a public URL, and a **forwarder** delivers them to your target. Provisioning means creating both, wiring the forwarder to the bucket, and confirming the topology. Both are `write` mutations — see [Safe mutations](/agents/safe-mutations).

```mermaid theme={null}
flowchart LR
    P[Webhook provider] -->|POST| B["Bucket<br/>(ingress URL)"]
    B --> F["Forwarder<br/>(egress + retries)"]
    F -->|delivers| T[Your target]
```

<Tip>
  For the common case, `repost init --name "Stripe production" --target https://api.example.com/webhooks --json` creates both halves in one **idempotent** call — re-running with the same name and target returns the existing route instead of duplicating it. Use the step-by-step flow below when you need to configure the bucket or forwarder beyond `init`'s defaults. Run `repost docs schema "init"` for its exact output shape.
</Tip>

## Create a bucket

The bucket's `internal_domain` is its receiving URL — point the provider at `https://<internal_domain>`. The name is a positional argument, not a flag.

```bash theme={null}
repost bucket create "Stripe production" \
  --response-mode QUEUE \
  --ack-status-code 200 \
  --json
```

```json theme={null}
{
  "schema": "repost.bucket.create/v1",
  "data": {
    "bucket": {
      "id": "bkt_01HV9S",
      "name": "Stripe production",
      "slug": "stripe-prod",
      "internal_domain": "stripe-prod.repost.sh",
      "is_active": true,
      "response_mode": "QUEUE",
      "ack_status_code": 200
    },
    "created": true
  }
}
```

Capture `bucket.id` (needed to attach a forwarder) and `bucket.slug` (used by every diagnostic command).

<Info>
  **`--response-mode`** decides how the bucket answers the provider:

  * `QUEUE` (default) — acknowledge immediately with `--ack-status-code` / `--ack-body`, then forward asynchronously. Best for reliability.
  * `PROXY` — forward synchronously and return the target's response, bounded by `--proxy-timeout-ms`. Use only when the provider needs the real downstream response.
</Info>

## Create a forwarder

Attach a forwarder to the bucket by **slug**. Bucket ID is accepted for compatibility, but agents should prefer the slug from `repost bucket list`. The name is positional; for an `EXTERNAL` forwarder (the default type), `--target` is required.

```bash theme={null}
repost forwarder create prod-api \
  --bucket stripe-prod \
  --target https://api.example.com/webhooks \
  --rate-limit-per-second 25 \
  --json
```

```json theme={null}
{
  "schema": "repost.forwarder.create/v1",
  "data": {
    "forwarder": {
      "id": "fwd_01HV9S",
      "bucket_id": "bkt_01HV9S",
      "name": "prod-api",
      "type": "EXTERNAL",
      "is_active": true,
      "is_paused": false,
      "path_mode": "LOCKED",
      "target_url": "https://api.example.com/webhooks",
      "http_timeout": 30000,
      "max_retries": 3,
      "retry_delay_ms": 15000,
      "retry_backoff": 2,
      "rate_limit_per_second": 25
    },
    "created": true
  }
}
```

Retry and rate behavior is set at creation. The defaults are sensible for most targets:

| Flag                      | Default   | Controls                                        |
| ------------------------- | --------- | ----------------------------------------------- |
| `--max-retries`           | `3`       | Delivery attempts before a row reaches the DLQ. |
| `--retry-delay-ms`        | `15000`   | Initial backoff before the first retry.         |
| `--retry-backoff`         | `2.0`     | Multiplier applied to each successive delay.    |
| `--retry-max-delay-ms`    | `900000`  | Ceiling on backoff (15 min).                    |
| `--rate-limit-per-second` | `0` (off) | Max deliveries/sec from this forwarder.         |
| `--http-timeout-ms`       | `30000`   | Per-attempt request timeout.                    |

<Note>
  `--rate-limit-per-second` is a **persistent** cap on all deliveries from this forwarder — it's what tames the burst when you [resume after a deploy](/agents/guard-deployments#deploy-then-resume). It is *not* the same as `replay --rate`, which throttles a single [replay job](/agents/replay-deliveries#execute-the-replay).
</Note>

## Provision idempotently

`init` is idempotent; the individual `create` commands are not.

<Warning>
  `bucket create` and `forwarder create` generate a fresh idempotency key each run, so a blind retry creates a **duplicate**. When you use them instead of `init`, check existence first and only create what is missing.
</Warning>

<Steps>
  <Step title="List before creating">
    `repost bucket list --json` and `repost forwarder list --bucket <bucket-slug> --json`. Match on `name` / `slug` / `target_url`.
  </Step>

  <Step title="Create only the gap">
    If the bucket exists, reuse its `id`. If a forwarder with that target exists, reuse it. Create only what the lists do not already contain.
  </Step>

  <Step title="Record the IDs">
    Persist the returned `bucket.id` and `forwarder.id` so later runs skip creation entirely.
  </Step>
</Steps>

## Inspect the topology

`bucket get` returns the bucket plus its active forwarders — the authoritative view of one route.

```bash theme={null}
repost bucket get stripe-prod --json
```

```json theme={null}
{
  "schema": "repost.bucket.get/v1",
  "data": {
    "bucket": {
      "id": "bkt_01HV9S",
      "slug": "stripe-prod",
      "internal_domain": "stripe-prod.repost.sh",
      "is_active": true
    },
    "forwarders": [
      {
        "id": "fwd_01HV9S",
        "name": "prod-api",
        "type": "EXTERNAL",
        "target_url": "https://api.example.com/webhooks",
        "is_active": true,
        "is_paused": false
      }
    ]
  }
}
```

## Verify end to end

Provisioning is not done until a real request flows through. Send a test webhook to `https://<internal_domain>`, then confirm both halves:

<CodeGroup>
  ```bash 1. Wait for the bucket to receive theme={null}
  repost expect --bucket stripe-prod --filter 'method:POST' --timeout 30s
  # → { "event_id": "evt_01JZ8V1", "bucket_id": "bkt_01HV9S", ... }
  ```

  ```bash 2. Confirm the forwarder delivered theme={null}
  repost forwards chain evt_01JZ8V1 --bucket stripe-prod --forwarder prod-api --json
  ```
</CodeGroup>

<Check>
  A clean `expect` (exit `0`) proves the bucket received the request. Running `forwards chain` on that `event_id` then shows the delivery attempt — a `SUCCEEDED` status proves the forwarder delivered. Together they confirm the route is live end to end. (Filter failures with `forwards search --failed`, not a `status:` query — see [Search & filters](/agents/search-and-filters#filters-for-forwards-search).)
</Check>

**Provisioning procedure**

1. Require `write` scope (`auth status` → `scopes` contains `write`).
2. Prefer `init --name <name> --target <url> --json` — idempotent, creates both halves. Otherwise:
3. `bucket list --json`; if no bucket matches the intended `name`/`slug`, `bucket create "<name>" … --json` and capture `data.bucket.id`. Else reuse the existing `id`.
4. `forwarder list --bucket <bucket-slug> --json`; if no forwarder matches the intended `target_url`, `forwarder create <name> --bucket <bucket-slug> --target <url> --json`. Else reuse it.
5. Never retry an individual `create` blindly — it duplicates. Re-list and reconcile instead.
6. Verify: `expect --timeout 30s` (exit `0`) returns an `event_id`, then `forwards chain <event_id> --json` shows a `SUCCEEDED` attempt.

## Continue

<Columns cols={3} className="gap-y-4">
  <Card title="Guard deployments" icon="shield-check" href="/agents/guard-deployments" cta="Protect routes" arrow="true">
    Pause and resume the forwarders you just created.
  </Card>

  <Card title="Diagnose webhooks" icon="search-check" href="/agents/diagnose-webhooks" cta="Watch traffic" arrow="true">
    Inspect what the new route receives and delivers.
  </Card>

  <Card title="Forwarders" icon="route" href="/forwarders/configuration" cta="Full configuration" arrow="true">
    Path modes, transforms, and the complete forwarder model.
  </Card>
</Columns>
