Provision targets

Create the route a webhook travels (a bucket that receives, a forwarder that delivers) idempotently, then 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.

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

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.

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.

repost bucket create "Stripe production" \
  --response-mode QUEUE \
  --ack-status-code 200 \
  --json
{
  "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).

--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.

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.

repost forwarder create prod-api \
  --bucket stripe-prod \
  --target https://api.example.com/webhooks \
  --rate-limit-per-second 25 \
  --json
{
  "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:

FlagDefaultControls
--max-retries3Delivery attempts before a row reaches the DLQ.
--retry-delay-ms15000Initial backoff before the first retry.
--retry-backoff2.0Multiplier applied to each successive delay.
--retry-max-delay-ms900000Ceiling on backoff (15 min).
--rate-limit-per-second0 (off)Max deliveries/sec from this forwarder.
--http-timeout-ms30000Per-attempt request timeout.

--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. It is not the same as replay --rate, which throttles a single replay job.

Provision idempotently

init is idempotent; the individual create commands are not.

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.

1
List before creating

repost bucket list --json and repost forwarder list --bucket <bucket-slug> --json. Match on name / slug / target_url.

2
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.

3
Record the IDs

Persist the returned bucket.id and forwarder.id so later runs skip creation entirely.

Inspect the topology

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

repost bucket get stripe-prod --json
{
  "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:

repost expect --bucket stripe-prod --filter 'method:POST' --timeout 30s
# → { "event_id": "evt_01JZ8V1", "bucket_id": "bkt_01HV9S", ... }

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.)

For agents

Provisioning procedure

  1. Require write scope (auth statusscopes 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