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.
Manage endpoints
Section titled “Manage endpoints”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 an endpoint
Section titled “Create an 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" }'endpoint, err := client.Webhooks.Create(ctx, forge.WebhookCreateParams{ URL: "https://hooks.example.com/managed-dev", EventTypes: []string{"deploy.completed", "deploy.failed", "category:security"}, Description: "Deploy + security alerts",})if err != nil { return err}
// Shown once — store it now.fmt.Println(*endpoint.Secret)mf webhooks create \ --url https://hooks.example.com/managed-dev \ --events 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.
{ "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..."}Event types & subscriptions
Section titled “Event types & subscriptions”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 omitevent_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.
The event envelope
Section titled “The event envelope”Every webhook body shares one envelope. The type tells you which event fired and
data carries the type-specific payload.
{ "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.
Delivery log & replay
Section titled “Delivery log & replay”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.
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:
{ "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.
Develop locally with mf listen
Section titled “Develop locally with mf listen”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:
mf listen --forward-to http://localhost:3000/webhooks --type deploy.completedOmit --type to forward everything. Once your handler works, register the real
HTTPS endpoint with mf webhooks create.
Best practices
Section titled “Best practices”-
Respond
2xxfast. managed.dev treats any2xxas 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. -
Process asynchronously. Enqueue the event and handle it on a worker. A slow handler causes timeouts, which trigger retries, which look like duplicate events.
-
Verify every payload. Reject any request whose
Forge-Signaturedoesn’t check out before you trust a single field. See verifying signatures. -
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. -
Expect new fields. Payloads are additive within the API version. Ignore unknown fields rather than failing on them.