Discord
Transports

Discord

Send messages via Discord Webhook API

The Discord transport is included in @betternotify/discord. It calls the Discord Webhook API via fetch — no external HTTP dependencies required (Node 22+ / Bun built-in).

It is a thin delivery adapter. Better-Notify handles rendering and validation upstream; discordTransport() POSTs the rendered message to the configured webhook URL.

Install

npm install @betternotify/discord

Getting your Webhook URL

  1. Open Server Settings → Integrations → Webhooks in your Discord server
  2. Click New Webhook and configure its name and channel
  3. Click Copy Webhook URL

The URL looks like: https://discord.com/api/webhooks/{id}/{token}

The webhook URL contains the authentication token. Keep it secret — anyone with this URL can post to your channel.

Usage

import { discordTransport } from '@betternotify/discord';

const transport = discordTransport({
  webhookUrl: process.env.DISCORD_WEBHOOK_URL!,
});

Pass it to createClient via transportsByChannel:

import { createClient } from '@betternotify/core';
import { discordChannel, discordTransport } from '@betternotify/discord';

const notify = createClient({
  catalog,
  transportsByChannel: {
    discord: discordTransport({ webhookUrl: process.env.DISCORD_WEBHOOK_URL! }),
  },
});

Standalone usage

The transport can be used directly without the full pipeline:

import { discordTransport } from '@betternotify/discord';

const transport = discordTransport({
  webhookUrl: process.env.DISCORD_WEBHOOK_URL!,
  wait: true,
});

const result = await transport.send(
  { body: 'Deployment complete!' },
  { route: 'manual', messageId: 'msg-1', attempt: 1 },
);

if (result.ok) {
  console.log(result.data.transportMessageId);
}

Options

Prop

Type

Embeds

Discord embeds let you send rich structured content — cards with titles, descriptions, colors, fields, images, and footers:

const catalog = rpc.catalog({
  deploy: rpc
    .discord()
    .input(z.object({ service: z.string(), version: z.string(), url: z.string() }))
    .body(({ input }) => `Deployed **${input.service}** v${input.version}`)
    .embeds(({ input }) => [
      {
        title: `${input.service} v${input.version}`,
        description: 'Deployment successful',
        url: input.url,
        color: 0x00ff00,
        fields: [
          { name: 'Service', value: input.service, inline: true },
          { name: 'Version', value: input.version, inline: true },
        ],
        timestamp: new Date().toISOString(),
      },
    ]),
});

Username and avatar overrides

Discord webhooks let you override the display name and avatar per-message. Set defaults at the transport level and override them per-route via channel slots:

const transport = discordTransport({
  webhookUrl: process.env.DISCORD_WEBHOOK_URL!,
  username: 'BetterNotify',
  avatarUrl: 'https://example.com/bot-avatar.png',
});

const catalog = rpc.catalog({
  alert: rpc
    .discord()
    .input(z.object({ level: z.enum(['info', 'error']) }))
    .body(({ input }) => `Alert: ${input.level}`)
    .username(({ input }) => (input.level === 'error' ? 'Error Bot' : 'Info Bot')),
});

Error handling

HTTP StatusError CodeMeaning
400VALIDATIONBad request body
401 / 403CONFIGInvalid webhook token
404CONFIGWebhook deleted or URL wrong
429PROVIDERRate limited
5xxPROVIDERDiscord server error
import { NotifyRpcError } from '@betternotify/core';

const result = await transport.send(rendered, ctx);

if (!result.ok && result.error instanceof NotifyRpcError) {
  switch (result.error.code) {
    case 'CONFIG':
      console.error('Check your webhook URL:', result.error.message);
      break;
    case 'PROVIDER':
      console.error('Discord issue, retry later:', result.error.message);
      break;
  }
}

On this page