Skip to main content

Outbound webhooks

Register a signed HTTPS receiver for Bex workspace events and verify Standard Webhooks deliveries.

Share
Last updated on September 23, 2026
On this page

Bex can POST thin, signed event notifications to an HTTPS endpoint you control. This guide covers the implemented registration contract and delivery identity. A runnable verifier is published as webhook-receiver (README). Personal email and push preferences are separate — see Notifications.

Source map (record these revisions when you refresh the guide):

ConcernAuthority
CMS docsthis repository (eden-cms-v2)
REST routes, create body, secret mintbex @ 0eb2036e2lego/backend/internal/webhooks/rest.go
Admin authorization, URL rules, resend…/webhooks/service.go
Thin payload + attempt headers…/webhooks/worker.go
HMAC scheme + reference vector…/webhooks/signing.go, signing_test.go
ProtocolStandard Webhooks

Replace placeholders such as $BEX_API_URL, $BEX_TOKEN, and $OWNER_ID with values from your installation. Do not paste real secrets into tickets or logs.

Who can administer endpoints

Webhook administration requires a workspace administrator user token with the OAuth scopes your installation expects for member-admin APIs. A machine API-key token has the developer role and cannot create or manage endpoints. See API permissions and Hosted setup for how tokens and workspaces relate. For a user-consented PKCE sample, see User-authorized OAuth integrations.

ownerId is the workspace id that owns the endpoint. Pass it on create and on every subsequent read that scopes to a workspace.

Choose events

List the live vocabulary (do not hard-code an outdated enum):

bash
curl --fail-with-body "$BEX_API_URL/v1/webhooks/event-types" \
  -H "Authorization: Bearer $BEX_TOKEN"

Common deploy transitions include deploy_started and deploy_ended. Many other service, datastore, and ops names appear in the same list. An empty eventFilter means “all events” after create-time normalization; prefer an explicit subset for production receivers.

Register an HTTPS endpoint

The destination must be a public https:// URL. Bex refuses private, loopback, and link-local addresses and does not follow redirects.

bash
curl --fail-with-body -X POST "$BEX_API_URL/v1/webhooks" \
  -H "Authorization: Bearer $BEX_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{
    \"ownerId\": \"$OWNER_ID\",
    \"name\": \"ops-pager\",
    \"url\": \"https://hooks.example.com/bex\",
    \"enabled\": true,
    \"eventFilter\": [\"deploy_started\", \"deploy_ended\"]
  }"

Required JSON fields: ownerId, name, url, enabled, and eventFilter (array; may be empty for all events). A successful create returns 201 with a body shaped like:

json
{
  "id": "whk-…",
  "name": "ops-pager",
  "url": "https://hooks.example.com/bex",
  "eventFilter": ["deploy_started", "deploy_ended"],
  "enabled": true,
  "secret": "whsec_…"
}

The secret field appears only on this create response. Store it as whsec_… and never log it. Later GET/PATCH responses omit the secret.

Manage endpoints from the dashboard Webhooks panel or with GET/PATCH/DELETE /v1/webhooks/{id}?ownerId=….

What each delivery contains

Bex signs every attempt with Standard Webhooks headers:

HeaderMeaning
webhook-idStable event id (evt-…). Same across retries of that event.
webhook-timestampUnix seconds for this attempt.
webhook-signaturev1, + base64 HMAC-SHA256 of id.timestamp.body with the raw secret bytes.

The body is intentionally thin — type, timestamp, and resource identifiers. Example shape (field names as delivered):

json
{
  "type": "deploy_ended",
  "timestamp": "2026-09-15T12:00:00Z",
  "data": {
    "id": "evt-…",
    "serviceId": "srv-…",
    "serviceName": "api",
    "status": "succeeded"
  }
}

data.status appears on terminal outcomes (succeeded / failed / canceled) and is omitted for nonterminal types. Hydrate authorized detail with the platform API (for example event or service reads documented under API / API reference) — do not assume the webhook body contains full resource state.

Verify the signature against the raw request bytes before parsing JSON. Reject tampered or unverifiable attempts with a non-2xx response so Bex can retry. Use webhook-id for receiver-side deduplication; attempt timestamps change on retry.

Delivery history and recovery

bash
curl --fail-with-body \
  "$BEX_API_URL/v1/webhooks/$WEBHOOK_ID/events?ownerId=$OWNER_ID" \
  -H "Authorization: Bearer $BEX_TOKEN"

Automatic retries use the worker's backoff schedule (implementation detail, not an SLA). After repeated failures an endpoint can be disabled — fix the receiver, then re-enable with PATCH /v1/webhooks/{id}?ownerId=… ("enabled": true).

Manual resend of a failed attempt:

bash
curl --fail-with-body -X POST \
  "$BEX_API_URL/v1/webhooks/$WEBHOOK_ID/events/$ATTEMPT_ID/resend?ownerId=$OWNER_ID" \
  -H "Authorization: Bearer $BEX_TOKEN" \
  -H "Idempotency-Key: resend-$(date +%s)"

Idempotency-Key must be 8–128 characters of letters, digits, ., _, :, or -. Resend identity is separate from the event's stable webhook-id: the receiver still deduplicates on webhook-id, while the platform uses your Idempotency-Key to avoid double-queueing the same admin resend.

Webhook delivery is asynchronous. Do not treat a successful deploy as proof that every subscriber processed the event.

Next step

Run examples/webhook-receiver/server.mjs with WEBHOOK_SECRET from the create response, confirm signature verification and durable deduplication locally, then enable the endpoint for production traffic.

Was this page helpful?

Run this on infrastructure you own

bex is the open-source, AI-native Render alternative — push a git repo and get a running HTTPS service on your own machines.

Get started with bex