Skip to content

Discord webhook integration

Route MAID operator announcements (maid ops announce) to a Discord channel using an incoming webhook. The fan-out is driven manually by the announce CLI — MAID does not automatically push maintenance, persistence, or AI-provider events to the webhook.

TL;DR: create the webhook in Discord → copy the URL into MAID_BRIDGES_WEBHOOK_URLS in the instance env file → verify with deploy/examples/discord-webhook-test.sh → send announcements with maid ops announce.


1. Create the webhook in Discord

  1. Open the channel you want alerts to appear in.
  2. Click the gear icon → Edit ChannelIntegrationsWebhooks.
  3. Click New Webhook, name it (e.g. maid-prod-alerts), pick an avatar if you like, and click Copy Webhook URL.

The URL looks like:

https://discord.com/api/webhooks/1234567890/abcdef...

Treat this URL as a secret. Anyone who has it can post to your channel as if they were the bot. Store it in /etc/maid/<instance>.env (mode 0640, owned root:maid-engine) — never commit it to git.

2. Configure MAID

Add the webhook URL to your instance env file, /etc/maid/<instance>.env — the same file the systemd unit loads via EnvironmentFile= and that the incident runbooks source. Keeping it in that one file (rather than a separate drop-in) is what guarantees maid ops announce sees the same MAID_BRIDGES_WEBHOOK_URLS the runbooks export. Use deploy/examples/discord-webhook.env.example as a reference for the line to add:

# Edit the instance env file in place; sudoedit preserves 0640
# root:maid-engine. Add the MAID_BRIDGES_WEBHOOK_URLS line shown below.
sudo -e /etc/maid/prod.env   # paste the real URL

Minimum env vars:

MAID_BRIDGES_WEBHOOK_URLS=discord:https://discord.com/api/webhooks/<ID>/<TOKEN>

The discord: prefix forces the Discord payload dialect ({"content": "..."}). The format is also auto-detected from any discord.com or discordapp.com host, but being explicit avoids surprises if the URL ever moves behind a proxy.

You can fan out to multiple destinations with a comma-separated list:

MAID_BRIDGES_WEBHOOK_URLS=discord:https://discord.com/api/webhooks/...,slack:https://hooks.slack.com/services/...

The announce CLI reads MAID_BRIDGES_WEBHOOK_URLS from the environment each time it runs, so no engine restart is required — just make sure the variable is exported into the environment of the shell that runs maid ops announce. A plain source file.env only sets shell variables; it does not export them to child processes such as maid ops announce or discord-webhook-test.sh. Use set -a (auto-export) around the source, or add export to each line in the env file:

# Confirm the value is visible to child processes that run announce:
set -a; source /etc/maid/prod.env; set +a
echo "$MAID_BRIDGES_WEBHOOK_URLS"

There is no engine-startup webhook log line — the bridge is instantiated on demand by the announce command, not wired into the running engine. If the value is malformed, announce logs a parse warning and skips the bad entry — double-check the format:url syntax above.

3. Test the webhook

Send a single test alert without involving the engine:

# Uses the URL pulled from MAID_BRIDGES_WEBHOOK_URLS in the env file.
# `set -a` exports it so the child script inherits it (a plain `source`
# would not):
set -a; source /etc/maid/prod.env; set +a
deploy/examples/discord-webhook-test.sh

# Or pass the URL explicitly:
deploy/examples/discord-webhook-test.sh \
    https://discord.com/api/webhooks/<ID>/<TOKEN>

# Or just print the payload, don't POST:
deploy/examples/discord-webhook-test.sh --dry-run

Expected output on success:

ok: Discord returned HTTP 204

And the Discord channel will show a line like:

[INFO] MAID webhook self-test — if you can read this, alert routing works. (actor: discord-webhook-test.sh) instance=maid-prod-01

Exit codes:

Code Meaning
0 webhook responded 2xx (or --dry-run succeeded)
1 bad arguments or URL missing
2 curl itself failed (DNS, TLS, connection refused)
3 Discord returned non-2xx (body is printed to stderr)

4. Troubleshoot

HTTP 401 Unauthorized

The webhook URL is wrong, was deleted, or the channel/server it lived in was deleted. Re-copy the URL from Discord → channel settings → Integrations → Webhooks.

HTTP 404 Not Found

Same as 401 in practice — the webhook ID Discord sees doesn't exist. Recreate the webhook and update the env var.

HTTP 429 Too Many Requests (rate limit)

Discord allows ~30 requests per minute per webhook URL (global limits per-channel are also enforced, see the Discord rate limit docs). Symptoms:

  • Test script prints error: Discord returned HTTP 429 plus a hint about the 30/min cap.
  • maid_webhook_send_errors_total{format="discord"} Prometheus counter increments.
  • Discord's response body includes a retry_after (seconds) field.

Mitigations, in order:

  1. Announce less often / batch. The webhook only fires when an operator runs maid ops announce, so the send rate is entirely under your control. Combine related notices into a single announcement rather than firing several in quick succession.
  2. Don't fan out to the same channel twice. MAID posts every message to every URL in MAID_BRIDGES_WEBHOOK_URLS (it does not round-robin), so adding a second Discord webhook on the same channel duplicates each announcement and makes the per-webhook 429 worse. Use additional endpoints only for genuinely different destinations (e.g. a Slack channel alongside Discord).
  3. Wait out retry_after. The channel retries once, and only on a 5xx response; 4xx responses (including 429) and connection errors are not retried, so a rate-limited send fails fast rather than compounding the rate-limit window. The retry count is fixed in code — there is no MAID_BRIDGES_WEBHOOK_MAX_RETRIES setting. If you hit a 429, honour the retry_after value before re-running maid ops announce.

curl: (6) Could not resolve host

DNS failure on the host. The bridges layer will surface this as an webhook[discord]: POST to ... failed log line and increment maid_webhook_send_errors_total{format="discord"}. Check the host's resolver and any outbound firewall rules:

dig +short discord.com
curl -fsS https://discord.com/api/webhooks/test 2>&1 | head -1

See also RB17 — firewall fallback.

Messages don't appear but exit code is 0

Discord accepted the payload but the channel might be muted for you, or the bot's avatar was replaced and your client cached it. Try posting from a different account / device. If the Audit Log shows the webhook executed, the issue is client-side.