Skip to content

Outbound webhooks

Outbound webhooks notify your HTTPS receiver when a root workflow execution reaches a terminal state. They replace polling for integrations that need a reliable completion signal; polling and GET /api/v1/executions/{id} remain available as the recovery/read path.

  1. Create an endpoint with POST /api/v1/webhooks/endpoints and save the returned signing_secret. It is shown only once.
  2. Verify signatures against the exact raw request bytes.
  3. Send a signed test with POST .../{endpoint_id}/test and inspect its delivery.
  4. Activate the endpoint with POST .../{endpoint_id}/activate.

Changing the destination with PATCH .../{endpoint_id} creates a new immutable URL revision, returns the endpoint to pending_verification, and suspends pending deliveries. Send a new signed test to that URL before activating again; a test against an older revision does not satisfy the gate.

All mutations require an Idempotency-Key header (8–255 characters). Repeating the same request does not create another endpoint, secret, test, retry, or replay. A replayed create/rotation never shows the secret again. State-setting operations are also safe when the requested state has already been reached: activating an active endpoint, pausing a paused endpoint, or archiving an archived endpoint succeeds as a no-op.

Terminal window
curl -X POST "$BASE_URL/api/v1/webhooks/endpoints" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: endpoint-prod-$(uuidgen)" \
-d '{
"name":"Production receiver",
"url":"https://example.com/madoo-webhooks",
"workflow_scope":"selected",
"workflow_ids":["wf_53fb2f6576fa4bc9a6d3c91a7e84de47"]
}'

The URL must be public HTTPS on port 443. Madoo rejects private/link-local addresses, redirects, embedded credentials and hosts that resolve to a non-public address. DNS is checked again and the resolved IP is pinned on every attempt.

Madoo uses the Standard Webhooks HMAC-SHA256 convention. Requests include:

  • webhook-id: stable event ID, also stable across retry/replay;
  • webhook-timestamp: Unix seconds;
  • webhook-signature: one or two space-separated v1,<base64> signatures during rotation.

The signed bytes are:

webhook-id + "." + webhook-timestamp + "." + raw_request_body

Remove whsec_ from the show-once secret and Base64-decode the remainder before computing HMAC. Accept a timestamp difference of at most five minutes, compare signatures in constant time, and deduplicate webhook-id before applying side effects. Never parse and re-serialize the JSON before verification.

An executable Node receiver is in docs/demo-workflows/webhook-completion-receiver.

The request is a CloudEvents 1.0 structured event:

  • com.madoo.execution.finished.v1 for completed, failed, cancelled, or partial_success;
  • com.madoo.webhook.test.v1 for a verification test.

Small text/JSON outputs may be included inline. Media are always references, never bytes. Every completion event contains result_url, which is the authoritative result read path, and trace_url, which explains how the run executed without embedding a potentially large trace. If the 20 KiB event budget is reached, outputs_truncated is true and the remaining outputs are available from that URL.

Any 2xx response succeeds. Redirects are not followed. Failures retry with durable exponential backoff until the configured deadline; Retry-After is honored. A real delivery receiving 410 disables the endpoint. Verification tests make one attempt only and never disable the endpoint.

The Webhook page and REST API expose each delivery and its attempts. retry is for an exhausted delivery; replay resends a delivered or exhausted delivery with the same event body and webhook-id, but a fresh timestamp/signature. A correctly idempotent receiver may ignore a replay whose event was already processed.

GET/POST /api/v1/webhooks/endpoints
GET /api/v1/webhooks/endpoints/{endpoint_id}
PATCH /api/v1/webhooks/endpoints/{endpoint_id}
POST /api/v1/webhooks/endpoints/{endpoint_id}/test
POST /api/v1/webhooks/endpoints/{endpoint_id}/activate
POST /api/v1/webhooks/endpoints/{endpoint_id}/pause
POST /api/v1/webhooks/endpoints/{endpoint_id}/rotate-secret
POST /api/v1/webhooks/endpoints/{endpoint_id}/revoke-previous-secret
DELETE /api/v1/webhooks/endpoints/{endpoint_id}
GET /api/v1/webhooks/deliveries[/{delivery_id}]
POST /api/v1/webhooks/deliveries/{delivery_id}/retry
POST /api/v1/webhooks/deliveries/{delivery_id}/replay
GET /api/v1/webhooks/activity

Use webhooks:read for read routes and webhooks:write plus the workspace webhook-management permission for mutations. MCP exposes the secret-free mutations and marks them as destructive so a client can require explicit confirmation; its OAuth consent text includes destination URL changes. The AI Agent and the in-editor AI Assistant expose webhook status in read-only mode until Madoo has a dedicated human-approval flow for webhook mutations. Create/rotate operations that reveal a secret remain REST/UI-only.