# Send alerts to Discord, Slack, Teams or your own server

> Post the alerts for your places to a Discord, Slack, Microsoft Teams or Google Chat channel, or to any HTTPS endpoint, and verify the signed JSON on your side.

Alerts and places · How-to guide · https://wxalerts.org/docs/alerts/webhooks/

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

1. 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.
2. On your [account](/account/#webhooks), open **Webhooks** and choose **Add a webhook**.
3. Name it, pick the type, paste the URL and tick the places it covers.
4. 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:

```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 of `timestamp + "." + raw body`, keyed with the secret.
- `X-WxAlerts-Delivery`: the delivery's id, also the same on every retry.

```python
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.
