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:
| 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. |
--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.
repost bucket list --json and repost forwarder list --bucket <bucket-slug> --json. Match on name / slug / target_url.
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.
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.)
Provisioning procedure
- Require
writescope (auth status→scopescontainswrite). - Prefer
init --name <name> --target <url> --json: idempotent, creates both halves. Otherwise: bucket list --json; if no bucket matches the intendedname/slug,bucket create "<name>" … --jsonand capturedata.bucket.id. Else reuse the existingid.forwarder list --bucket <bucket-slug> --json; if no forwarder matches the intendedtarget_url,forwarder create <name> --bucket <bucket-slug> --target <url> --json. Else reuse it.- Never retry an individual
createblindly: it duplicates. Re-list and reconcile instead. - Verify:
expect --timeout 30s(exit0) returns anevent_id, thenforwards chain <event_id> --jsonshows aSUCCEEDEDattempt.