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 theannounceCLI — 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_URLSin the instance env file → verify withdeploy/examples/discord-webhook-test.sh→ send announcements withmaid ops announce.
1. Create the webhook in Discord¶
- Open the channel you want alerts to appear in.
- Click the gear icon → Edit Channel → Integrations → Webhooks.
- 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:
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(mode0640, ownedroot: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:
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:
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 429plus 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:
- 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. - 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). - 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 noMAID_BRIDGES_WEBHOOK_MAX_RETRIESsetting. If you hit a 429, honour theretry_aftervalue before re-runningmaid 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:
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.