Skip to content

Webhooks

Webhooks let managed.dev push events to you instead of you polling for them. You register an HTTPS endpoint, subscribe it to the event types you care about, and managed.dev sends a signed POST to that URL whenever one of those events happens — a deploy lands, a build fails, malware is found, a backup restores. Every delivery is signed, logged, and replayable.

Webhooks pair naturally with async jobs: kick off a long-running mutation, get a 202 and a job back, then let a job.succeeded or job.failed webhook tell you when it finished — no polling loop required.

A webhook endpoint is a resource: a target URL, the set of event types it’s subscribed to, and a signing secret. Managing endpoints needs the webhooks:write scope; reads need webhooks:read.

Method + path Purpose
GET /v1/webhook-endpoints List your endpoints. Never returns secrets.
POST /v1/webhook-endpoints Create an endpoint and reveal its signing secret once.
GET /v1/webhook-endpoints/event-types The catalog of event types an endpoint can subscribe to.
GET /v1/webhook-endpoints/{id} Fetch a single endpoint (no secret).
PATCH /v1/webhook-endpoints/{id} Change the url, event_types, description, or enabled state.
DELETE /v1/webhook-endpoints/{id} Remove an endpoint and stop deliveries.
POST /v1/webhook-endpoints/{id}/roll Roll the signing secret; the new secret is revealed once.
GET /v1/webhook-endpoints/{id}/deliveries List delivery attempts, newest first.
POST /v1/webhook-endpoints/{id}/deliveries/{delivery_id}/replay Re-queue a past delivery.

The endpoint url must be HTTPS, on port 443 or 8443 — plain-HTTP receivers are rejected at create time.

Create a webhook endpoint
curl https://api.managed.dev/v1/webhook-endpoints \
-H "Authorization: Bearer mfk_live_..." \
-H "Forge-Version: 2026-06-23" \
-H "Content-Type: application/json" \
-d '{
"url": "https://hooks.example.com/managed-dev",
"event_types": ["deploy.completed", "deploy.failed", "category:security"],
"description": "Deploy + security alerts"
}'

The response carries the new endpoint and, on create only, the secret you’ll use to verify signatures. The secret is reveal-once — it’s never returned again on any read.

201 Created
{
"data": {
"id": "whe_01J7...",
"url": "https://hooks.example.com/managed-dev",
"event_types": ["deploy.completed", "deploy.failed", "category:security"],
"description": "Deploy + security alerts",
"enabled": true,
"secret": "whsec_9aF2...", // shown once, store it now
"created_at": "2026-06-23T18:04:11.412Z"
},
"request_id": "req_01J7..."
}

An endpoint’s event_types list controls what it receives, and a selector can be any of three shapes:

  • ["*"] — every customer event (the default when you omit event_types).
  • An exact type — "deploy.completed", "malware.detected".
  • A category selector — "category:security" subscribes to every type in that category, including types added to it later.

So a deploy-bot endpoint never sees malware events and a security endpoint never sees routine deploys. You can change the list any time with PATCH (it replaces the whole subscription set).

Build the list from the machine-readable catalog rather than hard-coding it: GET /v1/webhook-endpoints/event-types (or mf webhooks event-types) returns every subscribable type with its category, title, and severity. See event types & payloads for the catalog with payload notes, and the event-types reference for the full table.

Every webhook body shares one envelope. The type tells you which event fired and data carries the type-specific payload.

Common event envelope
{
"id": "evt_01J9...", // the event id — stable across redeliveries
"type": "deploy.completed",
"site_id": "site_01J7...", // null for account/team-level events
"env_id": "env_01J8...", // null when not environment-scoped
"created_at": "2026-06-23T18:05:02.118Z",
"data": { /* type-specific payload */ }
}

Delivery is at-least-once. Each HTTP request also carries a Forge-Delivery-Id header that stays stable across retries and replays — that header is your dedupe key (see best practices).

The same events are queryable over the API: GET /v1/events (events:read) lists recent events for your account so you can backfill or reconcile after downtime, independent of any endpoint.

Every attempt to reach your endpoint is recorded — the response status, the error if any, and how many attempts it took. Read the log to debug a misbehaving consumer, and replay any delivery once your endpoint is healthy again.

Inspect recent deliveries
curl "https://api.managed.dev/v1/webhook-endpoints/whe_01J7.../deliveries?limit=20" \
-H "Authorization: Bearer mfk_live_..."

Each delivery in the (cursor-paginated) list looks like:

A delivery record
{
"id": "whd_01J9...",
"event_id": "evt_01J9...",
"status": "failed", // pending | delivered | failed
"attempts": 3,
"response_status": 500, // HTTP status of the last attempt
"last_error": "server returned 500",
"created_at": "2026-06-23T18:05:02.204Z",
"delivered_at": null,
"next_attempt_at": "2026-06-23T18:09:02.204Z"
}

A replay (POST .../deliveries/{delivery_id}/replay, or mf webhooks replay <endpointID> <deliveryID>) re-queues the delivery: its status resets to pending and the attempt counter and backoff are cleared. The re-sent request carries the same Forge-Delivery-Id and the same event body, signed fresh at send time.

The webhook endpoint detail page in the app.managed.dev dashboard: the endpoint URL and subscribed event types at the top, then a delivery log table with columns for timestamp, event type, response status, attempt count, and a per-row “Replay” button.

You don’t need a public URL to build a consumer. mf listen watches your account’s event feed and forwards each event to a local URL as it happens:

Forward events to a local handler
mf listen --forward-to http://localhost:3000/webhooks --type deploy.completed

Omit --type to forward everything. Once your handler works, register the real HTTPS endpoint with mf webhooks create.

  1. Respond 2xx fast. managed.dev treats any 2xx as accepted and anything else (or a timeout) as failed and retriable. Acknowledge the delivery and return immediately — don’t do real work inside the request.

  2. Process asynchronously. Enqueue the event and handle it on a worker. A slow handler causes timeouts, which trigger retries, which look like duplicate events.

  3. Verify every payload. Reject any request whose Forge-Signature doesn’t check out before you trust a single field. See verifying signatures.

  4. Dedupe on Forge-Delivery-Id. Delivery is at-least-once — retries and manual replays re-send the same delivery, and the header stays stable across all of them. Record processed ids and skip ones you’ve already seen.

  5. Expect new fields. Payloads are additive within the API version. Ignore unknown fields rather than failing on them.