Webhooks
Configure a webhook on a service when your application prefers a callback over polling. Every run of that service is reported to your endpoint with a signed POST once it reaches a terminal state. The stored run remains the source of truth; delivery is best effort with retries.
Configure the endpoint
Section titled “Configure the endpoint”In the console, open the service, scroll to Webhook in its settings, enter an HTTPS endpoint URL and a signing secret (or press Generate), then save. The secret is stored encrypted and never shown again; enter a new value to rotate it.
With the SDK:
await magmell.patchServiceSettings('reviewer', { webhookUrl: 'https://example.com/hooks/magmell', webhookSecret: '<16-256 printable ASCII characters>',})Rules for the URL: https:// only, a public hostname or address, no credentials in the
URL, at most 2048 bytes. Targets that resolve to private, loopback, link-local, metadata,
multicast or other special-use addresses are rejected when you configure them and again
before every delivery. Redirects are not followed.
Removing the webhook (webhookUrl: null, or Remove webhook in the console) clears the
secret as well. Deliveries still pending at that moment are skipped.
Send a test event
Section titled “Send a test event”Send test event in the console, or await magmell.testWebhook('reviewer'), delivers a
webhook.test payload through exactly the same signing path as a real report and shows the
result: the HTTP status your endpoint returned and the round-trip time, or the error class
(timeout, connect_error, dns, blocked_target, …). Use it to verify your signature check before
relying on the webhook. Test events and webhook URL changes are each limited to ten per
minute per team.
Payload
Section titled “Payload”{ "event": "run.succeeded", "run_id": "8ebea6be-3b54-40ca-b8b5-5176264f2450", "run_number": 42, "service": "reviewer", "deployment_revision": 3, "session_key": null, "status": "succeeded", "result": { "summary": "..." }, "result_omitted": false, "error": null, "queued_at": "2026-09-03T14:32:01.104512+00:00", "started_at": "2026-09-03T14:32:03.517209+00:00", "finished_at": "2026-09-03T14:32:07.882014+00:00"}event is run.succeeded or run.failed; a failed run carries its redacted message in
error. A result larger than 256 KiB as stored is not included: result is null and
result_omitted is true, and you fetch the run instead. A test event has the same keys
with event: "webhook.test" and nulls elsewhere.
Every request carries:
Content-Type: application/jsonUser-Agent: magmell-webhook/1X-Magmell-Event: run.succeededX-Magmell-Timestamp: 1788791527X-Magmell-Signature: v1=<hex digest>Verify the signature
Section titled “Verify the signature”The signature is HMAC-SHA256, keyed with your signing secret, over the timestamp, a period, and the exact raw request body. Compute it from the raw bytes before you parse anything:
import { createHmac, timingSafeEqual } from 'node:crypto'
function verify(body: Buffer, timestamp: string, received: string, secret: string) { const expected = 'v1=' + createHmac('sha256', secret) .update(`${timestamp}.`) .update(body) .digest('hex') const a = Buffer.from(expected) const b = Buffer.from(received) return a.length === b.length && timingSafeEqual(a, b)}Reject requests whose X-Magmell-Timestamp is outside your replay window (five minutes is a
reasonable default). Rotating the secret applies to every delivery attempted after the
change, including retries of earlier runs.
Delivery behavior
Section titled “Delivery behavior”A 2xx response marks the report delivered. Any other status, a timeout, a connection error
or an unresolvable host is retried with backoff: 10 s, 40 s, 160 s, 640 s and 900 s, six
attempts in total over roughly half an hour, after which the run’s delivery is failed.
Your endpoint has ten seconds to answer; the response body is never read. Deliveries are
made one at a time per team, in due order, so a slow endpoint on one of your services
delays only your own reports.
Delivery is at-least-once. A retry can deliver the same completion more than once, so make
your receiver idempotent using run_id.
The run record shows the delivery state under webhook: status (pending, delivered,
failed or skipped), attempts, the last HTTP status, and the last error class. The
console shows the same line on the run’s page.
Runs accepted while no webhook was configured are never reported, even if you configure one later.