Melso Docs

Webhooks

Verified request, response and authorization contracts.

Subscribe to Task outcomes without remembering the latest Run ID. All five events below are retained; no additional event types are introduced.

EventDurable triggerExtra summary fields
task.deliveredCompleteRun commits a substantive reply/attachment or branch and sets the open Task’s review marker; empty outcomes and registered child-wait handoffs do not count.None
task.needs_inputA pending interaction is inserted. Emitted per interaction, including while its source Run is still active.interaction_id, interaction_kind
task.failedA Run becomes failed, or cancelled with cloud_worker_unavailable (the Task API’s Cloud failure classification), and commits without an automatic retry child. Nonretryable failures qualify; closed Tasks suppress this event.failure_reason
task.completedTask completed_at changes from null to set, including a GitHub close-intent merge or platform review completion.None
task.cancelledTask cancelled_at changes from null to set. Cancelling only a Run does not qualify.None

Reopening is intentionally not an event: it permits future work but is not a review/input/terminal outcome. Poll current Task details when you need all intermediate transitions. A delivery payload is a historical snapshot, not a promise that the Task remains in that state when your receiver processes it.

Payload

{"id":"event-uuid","type":"task.delivered","created_at":"2026-09-23T12:00:00Z","workspace_id":"workspace-uuid","task_id":"task-uuid","task_identifier":"MELS-115","run_id":"run-uuid","task_status":"to_review","run_status":"completed"}

id, workspace_id, task_id are UUID strings; type, task_identifier, task_status are strings; created_at is a timestamp. run_id and run_status can be null when a Task closes without a Run. task_status follows normal Task precedence, so a pending ask can carry working while its Run exits. Payloads omit prompt text, answers and secrets; fetch details using the Task and interaction APIs.

Signing and delivery

Each POST carries Content-Type: application/json, Melso-Event-ID, and Melso-Signature: t=UNIX_SECONDS,v1=HEX_DIGEST. Compute HMAC-SHA256 using the literal secret string over timestamp + "." + raw_request_body. Compare signatures in constant time, enforce a timestamp tolerance (for example five minutes), then de-duplicate by event id. Do not parse/re-serialize JSON before verification. Timestamps/signatures change on retries; event IDs and payloads stay fixed.

The workspace secret is shared by registered endpoints and per-task callbacks. First endpoint creation initializes it and reveals it once. Subsequent creates omit signing_secret. Rotation returns a new secret once and immediately replaces the key for future attempts, including retries; an already in-flight request can still use the old key. Coordinate receiver rotation accordingly.

State changes write payloads and recipients transactionally to an outbox; a separate worker POSTs them. A 2xx acknowledges delivery. Every other status, redirect, network error or timeout retries with exponential backoff (30 seconds, doubling to one hour, plus 0–25% jitter) until the 24-hour window expires. Workers recover expired one-minute leases. Delivery is at least once within this window, with possible duplicates and no ordering guarantee; permanently unavailable receivers end in failed. Resend opens another 24-hour window with the same event ID and cumulative attempt count.

HTTPS is required in all environments. Registration and the connection dialer reject private, loopback, link-local, metadata and reserved addresses, including mixed public/private DNS answers. Delivery pins the validated address, disables proxies/redirects and limits requests to 15 seconds. The log stores at most 4 KiB of UTF-8 response text. Removing/disabling an endpoint suppresses queued future attempts, but cannot recall an in-flight request. Endpoint edits affect future events; queued deliveries retain their original URL. Task callbacks receive all five events and are independent of endpoint subscriptions.

Self-hosted servers must set MELSO_OUTBOUND_WEBHOOK_SECRET_KEY to a base64-encoded random 32-byte key before registering endpoints or callbacks. The server stores workspace signing secrets encrypted. Keep the deployment key stable across replicas/restarts.

All management routes below require a human workspace owner/admin; they reject Run and MCP actors. Recent deliveries return at most 100 rows, newest first. The log keeps cumulative attempts and the most recent response/status, not a separate row for each network attempt.

Examples use the environment variables from Overview. UUID placeholders must be replaced with your own IDs.

GET /api/workspaces/{workspaceId}/webhooks

Auth: Human workspace owner/admin; Run and MCP actors rejected.

Request: None.

Response: {id:UUID,workspace_id:UUID,url:string,description:string,event_types:string[],enabled:boolean,created_at:timestamp} array.

Status: 200. Authentication and resource-access errors follow Overview.

curl -sS -X GET "$MELSO_URL/api/workspaces/$WORKSPACE_ID/webhooks" \
  -H "Authorization: Bearer $MELSO_TOKEN" -H "X-Workspace-ID: $WORKSPACE_ID"

POST /api/workspaces/{workspaceId}/webhooks

Auth: Human workspace owner/admin; Run and MCP actors rejected.

Request: url:HTTPS URL, event_types:string[] (1–5 supported event names), description?:string (max 500), enabled?:boolean (default true).

Response: {endpoint:Endpoint,signing_secret?:string}; secret only when first initialized.

Status: 201; 400 URL/DNS/events invalid; 503 signing key unavailable. Authentication and resource-access errors follow Overview.

curl -sS -X POST "$MELSO_URL/api/workspaces/$WORKSPACE_ID/webhooks" \
  -H "Authorization: Bearer $MELSO_TOKEN" -H "X-Workspace-ID: $WORKSPACE_ID" \
  -H "Content-Type: application/json" -d '{"url":"https://receiver.example.com/melso","description":"Factory","event_types":["task.delivered","task.needs_input","task.failed","task.completed","task.cancelled"]}'

PUT /api/workspaces/{workspaceId}/webhooks/{endpointId}

Auth: Human workspace owner/admin; Run and MCP actors rejected.

Request: Same fields as creation; full replacement. enabled defaults true when omitted.

Response: {id:UUID,workspace_id:UUID,url:string,description:string,event_types:string[],enabled:boolean,created_at:timestamp}.

Status: 200; 400 invalid URL/events; 404 missing endpoint. Authentication and resource-access errors follow Overview.

curl -sS -X PUT "$MELSO_URL/api/workspaces/$WORKSPACE_ID/webhooks/$ENDPOINT_ID" \
  -H "Authorization: Bearer $MELSO_TOKEN" -H "X-Workspace-ID: $WORKSPACE_ID" \
  -H "Content-Type: application/json" -d '{"url":"https://receiver.example.com/melso","event_types":["task.delivered"],"enabled":false}'

DELETE /api/workspaces/{workspaceId}/webhooks/{endpointId}

Auth: Human workspace owner/admin; Run and MCP actors rejected.

Request: Path: endpoint UUID.

Response: Empty.

Status: 204; 404 missing endpoint. Authentication and resource-access errors follow Overview.

curl -sS -X DELETE "$MELSO_URL/api/workspaces/$WORKSPACE_ID/webhooks/$ENDPOINT_ID" \
  -H "Authorization: Bearer $MELSO_TOKEN" -H "X-Workspace-ID: $WORKSPACE_ID"

POST /api/workspaces/{workspaceId}/webhooks/signing-secret/rotate

Auth: Human workspace owner/admin; Run and MCP actors rejected.

Request: No body. Also initializes a signing key for callback-only use.

Response: {signing_secret:string} shown once.

Status: 200; 503 signing key unavailable. Authentication and resource-access errors follow Overview.

curl -sS -X POST "$MELSO_URL/api/workspaces/$WORKSPACE_ID/webhooks/signing-secret/rotate" \
  -H "Authorization: Bearer $MELSO_TOKEN" -H "X-Workspace-ID: $WORKSPACE_ID" \
  -H "Content-Type: application/json" -d '{}'

GET /api/workspaces/{workspaceId}/webhooks/deliveries

Auth: Human workspace owner/admin; Run and MCP actors rejected.

Request: None. Latest 100 deliveries.

Response: Array of {id,event_id,endpoint_id:UUID|null,url,status,attempts,response_code:integer|null,latency_ms:integer|null,response_body,error,created_at,next_attempt_at,payload}. IDs are UUID strings, attempts is an integer; time fields are timestamps. Status is pending/delivering/succeeded/failed/disabled.

Status: 200. Authentication and resource-access errors follow Overview.

curl -sS -X GET "$MELSO_URL/api/workspaces/$WORKSPACE_ID/webhooks/deliveries" \
  -H "Authorization: Bearer $MELSO_TOKEN" -H "X-Workspace-ID: $WORKSPACE_ID"

POST /api/workspaces/{workspaceId}/webhooks/deliveries/{deliveryId}/resend

Auth: Human workspace owner/admin; Run and MCP actors rejected.

Request: Path: delivery UUID.

Response: {status:"pending"}.

Status: 202; 409 absent, disabled, or currently leased delivery. Authentication and resource-access errors follow Overview.

curl -sS -X POST "$MELSO_URL/api/workspaces/$WORKSPACE_ID/webhooks/deliveries/$DELIVERY_ID/resend" \
  -H "Authorization: Bearer $MELSO_TOKEN" -H "X-Workspace-ID: $WORKSPACE_ID" \
  -H "Content-Type: application/json" -d '{}'

Local receiver

Run MELSO_WEBHOOK_SECRET=whsec_… python3 examples/webhooks/receiver.py from the repository, then expose port 8787 through a public HTTPS tunnel. Register that tunnel URL. The example verifies the raw request body, rejects timestamps older than five minutes, persists event IDs in SQLite and returns 204 for duplicates. It uses only the Python standard library. Production receivers should atomically persist the event and enqueue their work before acknowledging.