Alert webhooks
The webhook destination contract: request headers, the JSON payload for each event, verifying the HMAC signature, retries, and idempotency.
A webhook notification destination sends every alert event to a URL you own. This page is the contract your endpoint can rely on.
The request
Maple sends a POST with a JSON body and these headers:
| Header | Value |
|---|---|
content-type | application/json |
x-maple-event-type | trigger, resolve, renotify or test |
x-maple-delivery-key | A stable ID for this delivery. Retries reuse it; use it to deduplicate. |
x-maple-signature | Hex HMAC-SHA256 of the raw body. Only sent when a signing secret is set. |
Respond with any 2xx within 15 seconds. The response body is ignored.
Events
| Event | Sent when |
|---|---|
trigger | An incident opens, after the rule’s required number of consecutive breaching checks |
renotify | An incident is still breaching after the rule’s re-notify interval (30 minutes by default) |
resolve | An incident closes, after the rule’s required number of consecutive healthy checks |
test | You press Send test on the destination, or test a rule with notifications on |
Payload
{
"eventType": "trigger",
"incidentId": "b6f1…",
"incidentStatus": "open",
"dedupeKey": "org_…:alrt_…:checkout",
"rule": {
"id": "alrt_…",
"name": "Checkout error rate",
"signalType": "error_rate",
"severity": "critical",
"groupKey": "checkout",
"comparator": "gt",
"threshold": 5,
"thresholdUpper": null,
"windowMinutes": 5
},
"observed": { "value": 8.4, "sampleCount": 1250 },
"template": null,
"chart": { "url": "https://…" },
"linkUrl": "https://app.maple.dev/alerts/…",
"chatUrl": "https://app.maple.dev/chat?…",
"sentAt": "2026-09-25T09:14:00.000Z",
"event": { "specversion": "1.0", "type": "dev.maple.alert.lifecycle.trigger.v1", "…": "…" }
}
| Field | Description |
|---|---|
eventType | Same as the x-maple-event-type header |
incidentId | The incident this event belongs to. null for tests. |
incidentStatus | open or resolved |
dedupeKey | Stable for one incident across trigger, renotify and resolve. Use it to thread events together. |
rule.signalType | error_rate, p95_latency, p99_latency, apdex, throughput, builder_query or raw_query |
rule.severity | warning or critical |
rule.groupKey | The group that breached, such as a service name. __total__ for an ungrouped rule. |
rule.comparator | gt, gte, lt, lte, eq, neq, between or not_between |
rule.threshold | The threshold, or the lower bound for between and not_between |
rule.thresholdUpper | The upper bound for between and not_between, otherwise null |
rule.windowMinutes | The evaluation window |
observed.value | The value the check measured |
observed.sampleCount | How many data points the value was computed from |
template | The rule’s rendered notification template, or null |
chart | url of a chart image and/or a sparkline, or null |
linkUrl | The incident in the Maple dashboard |
chatUrl | Opens Maple’s AI chat with the incident as context |
sentAt | ISO-8601 time the event was first generated. Retries keep the original value. |
event | The same event as a CloudEvents 1.0 envelope, for routers that speak CloudEvents |
The body is identical for every destination’s webhook, regardless of the rule’s notification template. Treat unknown fields as additive: new fields can appear without notice.
Error issue notifications
A destination listed in your organization’s error notification policy (set with the update_error_notification_policy MCP tool or the API) also receives error issue events: a new issue, a regression, a resolve, and optionally workflow changes and claims. They carry the same headers and a smaller body: eventType, incidentId, incidentStatus, dedupeKey, rule, observed, linkUrl, chatUrl and sentAt, without event, template or chart. Here rule.id is the issue ID, rule.name reads <ExceptionType> in <service>, and observed.value is the occurrence count. dedupeKey starts with error:.
Issues escalated by triage arrive with eventType: "escalation" in the body and an extra escalation object describing the issue. The x-maple-event-type header on these reads trigger, so branch on the body’s eventType.
Verifying the signature
Set a signing secret on the destination and Maple signs each body. The signature is the lowercase hex HMAC-SHA256 of the exact bytes of the request body, keyed with the secret, with no prefix. Compute it over the raw body before parsing the JSON, and compare in constant time.
import { createHmac, timingSafeEqual } from "node:crypto"
export function isFromMaple(rawBody: Buffer, signature: string | undefined, secret: string): boolean {
if (!signature) return false
const expected = createHmac("sha256", secret).update(rawBody).digest()
const received = Buffer.from(signature, "hex")
return received.length === expected.length && timingSafeEqual(received, expected)
}
import hashlib, hmac
def is_from_maple(raw_body: bytes, signature: str | None, secret: str) -> bool:
if not signature:
return False
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
The signature covers the body only, and the body carries no delivery timestamp you can trust for freshness, so a captured request could be replayed. Deduplicate on x-maple-delivery-key to make replays harmless.
Retries and failures
| Your response | What Maple does |
|---|---|
2xx | Delivered |
408, 429, 5xx, timeout, connection error | Retries with backoff |
401, 403 | Fails without retrying (authentication problem) |
404, 410 | Fails without retrying (endpoint gone) |
Any other 4xx | Fails without retrying (rejected) |
Alert events are attempted up to 5 times, about 1, 2, 4 and 8 minutes apart, with every retry carrying the same body and x-maple-delivery-key. Send test is attempted once.
After 3 consecutive non-retryable failures, the destination is disabled and the reason is shown on it under Alerts → Destinations. Fix the endpoint and re-enable it; a successful delivery resets the count.
Endpoint checklist
- Read the raw body, verify the signature, then parse.
- Return
2xxquickly and do slow work asynchronously; anything past 15 seconds is a failed attempt. - Deduplicate on
x-maple-delivery-key; retries are normal. - Group by
dedupeKeyto follow one incident fromtriggertoresolve. - Accept
testevents so Send test succeeds.