Channels
Discord notifications
Post Ray notifications to a Discord channel through a channel webhook. Discord setup, config, markdown content, mention safety, escaping and errors.
The discord_webhook channel posts messages to a Discord text channel through a Discord webhook URL. The webhook decides the channel, so there is no per-message recipient. API calls go to https://ray-api.gege.mn with Authorization: Bearer $RAY_API_KEY.
| Channel type | discord_webhook |
| Template kind | discord_text |
| Recipient | {} |
| Pacing | None (Discord rate limits are retried) |
| Suppression | None |
What you need from Discord
You need the Manage Webhooks permission on the server.
- In Discord open Server Settings, Integrations, Webhooks.
- Click New Webhook, give it a name (shown as the message author) and pick the channel.
- Click Copy Webhook URL. It looks like
https://discord.com/api/webhooks/123456789012345678/AbCdEf....
The token in the URL is the secret: anyone with it can post to the channel. For several channels, create one webhook and one Ray channel config per channel.
Channel config
Create the config in the dashboard: Channels, New channel, Discord webhook. The URL is stored encrypted; after saving, the dashboard only shows its host.
| Field | Required | Notes |
|---|---|---|
| Name | yes | Internal label, for example release-notes. |
Webhook URL (webhookUrl) |
yes | Must be on discord.com or discordapp.com. Any other host is refused when sending (the delivery fails as failed_terminal). When editing, leave blank to keep the stored URL. |
Find the config's id with GET /channels; its kind is discord_webhook.
Template content
| Field | Type | Notes |
|---|---|---|
content |
string, 1 to 2000 | Discord markdown: **bold**, *italic*, __underline__, ~~strike~~, `code`, > quote, # heading, - list, [text](https://example.com). |
Escaping: Ray puts a backslash before \ ` * _ ~ | > # - in param values, so a value renders literally and can't add formatting.
Mentions are always disabled: Ray sends every message with Discord's allowed_mentions set to parse nothing, so @everyone, @here, user and role mentions render as plain text and ping nobody, whether they come from the template or from params.
Recipient
Always an empty object:
{}
Send a message
curl -X POST https://ray-api.gege.mn/send \
-H "Authorization: Bearer $RAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"channelConfigId": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
"content": {
"content": "**Release {{version}}** is out\n> {{summary}}\n[Changelog](https://example.com/changelog)"
},
"params": { "version": "2.4.0", "summary": "Faster exports and a new dark theme" },
"recipient": {}
}'
const res = await fetch("https://ray-api.gege.mn/send", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.RAY_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": "release-2.4.0-discord",
},
body: JSON.stringify({
channelConfigId: "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
templateId: "1d2e3f4a-5b6c-4d7e-8f9a-0b1c2d3e4f5a", // a discord_text template
params: { version: "2.4.0", summary: "Faster exports and a new dark theme" },
recipient: {},
}),
});
if (res.status !== 202) throw new Error(`Ray ${res.status}: ${await res.text()}`);
Note the nesting: the send's content object has a single field that is also called content.
With escaping, the 2.4.0 param renders as-is, while a summary like - fixed *all* bugs would appear literally instead of as a list item with italics.
Discord returns no message id, so the delivery row's providerMessageId is discord-<rowId>. delivered means Discord accepted the message.
Limits
Discord caps message content at 2000 characters and rate-limits webhooks. Ray does not pace Discord sends; when Discord answers 429, the delivery is retried with backoff. For bursts of many messages to one channel, combine them into one message or use priority: "low" for the non-urgent ones.
Errors
Errors appear on the delivery row as providerError: { "name": "DiscordWebhookError", "message": "Discord webhook failed: <status> <statusText> <body>" }.
| Result | Cases |
|---|---|
failed_terminal (not retried) |
Any HTTP 4xx except 429, such as 401 Invalid Webhook Token, 404 Unknown Webhook (webhook deleted), 400 invalid body (for example content over 2000 characters after rendering); a URL that isn't on discord.com or discordapp.com or isn't valid |
failed_retryable (retried) |
HTTP 429, HTTP 5xx, network errors and timeouts |
Common fixes:
Unknown Webhook: the webhook was deleted in Discord. Create a new one and update the config.400with a content length error: rendered params pushed the message over 2000 characters. Shorten the template or the params.refusing to POST to non-Discord host: the configured URL isn't a Discord webhook URL.
Request errors from POST /send are listed in Errors.