A webhook posts each alert for your places to a chat channel or a server you run. One webhook can cover several places, and one place can feed several webhooks. Webhooks are free on every plan.
Add one
- Create an incoming webhook in the destination and copy its URL:
- Discord: Channel settings, Integrations, Webhooks, New Webhook.
- Slack: an app with Incoming Webhooks turned on, added to the channel.
- Microsoft Teams: the Workflows app, “Post to a channel when a webhook request is received”.
- Google Chat: Space settings, Apps and integrations, Webhooks.
- Anything else (Zapier, IFTTT, your own code): any public HTTPS URL.
- On your account, open Webhooks and choose Add a webhook.
- Name it, pick the type, paste the URL and tick the places it covers.
- Choose Save and send test to post a test message straight away.
The type is checked against the URL, so a Slack address cannot be saved as Discord. After saving, the URL is shown only by its host.
What gets posted
One message per alert per webhook. When an alert covers several of the webhook’s places, the one message names them all (“Tornado Warning for Home, Shop and Barn”). Each chat platform gets its own format: a Discord embed, Slack Block Kit, a Teams card or a Google Chat card, with the headline, a colour for the severity, when it ends and a link to the alert. Discord messages never ping @everyone or a role.
When delivery fails
A busy or briefly down receiver is retried five times over about fifty minutes,
and never sooner than its own Retry-After asks. A URL that answers “not
found” or “forbidden” is not retried. After three alerts in a row fail to land, the webhook shows
Delivery failing and you get one email. The next success clears it.
Generic webhooks
Anything that is not a chat platform gets versioned JSON:
{
"type": "alert",
"version": 2,
"event_id": "5f0c1c8e-0d0e-4b7a-9b7e-2f1d8c6a4e11",
"alert": {
"id": "urn:oid:2.49.0.1.840.0.example",
"event": "Tornado Warning",
"headline": "Tornado Warning issued for Santa Rosa County until 4:15 PM CDT",
"significance": "W",
"severity": "Extreme",
"issued_at": "2026-04-02T20:41:00Z",
"ends_at": "2026-04-02T21:15:00Z",
"link": "https://wxalerts.org/alerts/..."
},
"place": { "id": 12, "label": "Home" },
"places": [{ "id": 12, "label": "Home" }, { "id": 14, "label": "Shop" }],
"branding": "Alerts by WxAlerts.org"
}
event_id is the same on every retry of one message, so use it to ignore a
retry you already handled. place is the first place, kept for receivers built
on version 1; read places. A test message has "type": "test".
Verify the signature
The signing secret is shown once, when you create the webhook. Save it then. Every request carries three headers:
X-WxAlerts-Timestamp: Unix seconds.X-WxAlerts-Signature:sha256=and the hex HMAC-SHA256 oftimestamp + "." + raw body, keyed with the secret.X-WxAlerts-Delivery: the delivery’s id, also the same on every retry.
import hashlib, hmac, time
def verify(secret: str, timestamp: str, signature: str, body: bytes) -> bool:
if abs(time.time() - int(timestamp)) > 300:
return False
expected = hmac.new(secret.encode(), f"{timestamp}.".encode() + body,
hashlib.sha256).hexdigest()
return hmac.compare_digest(f"sha256={expected}", signature)
Reject anything that fails. Without this check, anyone who learns the URL can post a fake warning to it.
If you show these alerts to people, credit WxAlerts.org; the branding field
carries the line to use.

