# @suaveplan/discord-client - Generic Discord API client for webhooks, bot messaging, and embeds with injectable fetch, OpenTelemetry instrumentation, and structured error codes Generic Discord API client for webhooks, bot messaging, and embeds. Runtime-agnostic via injectable `fetch` and `clock`. Maps every documented Discord failure mode to a stable `DISCORD.*` error code and emits OpenTelemetry counters and histograms for every request. ## Documentation tree - [bot-client](./src/core/bot-client/discord-bot-client.md) — Implements the bot-token half of `@suaveplan/discord-client`. Wraps the Discord Bot REST API (`/channels/:id/messages`,… - [discord-bot-client](./src/core/bot-client/discord-bot-client.md) — Implements the bot-token half of `@suaveplan/discord-client`. Wraps the Discord Bot REST API (`/channels/:id/messages`,… - [webhook](./src/core/webhook/discord-webhook.md) — Implements the webhook half of `@suaveplan/discord-client`. Discord webhooks are URLs that anyone with the URL can POST… - [discord-webhook](./src/core/webhook/discord-webhook.md) — Implements the webhook half of `@suaveplan/discord-client`. Discord webhooks are URLs that anyone with the URL can POST… - [errors](./src/errors/discord-client-error.md) — Defines the namespaced, frozen error-code registry and the typed `DiscordClientError` class used by every runtime failu… - [discord-client-error](./src/errors/discord-client-error.md) — Defines the namespaced, frozen error-code registry and the typed `DiscordClientError` class used by every runtime failu… - [http](./src/utils/http.md) — Single funnel through which every Discord API and webhook request flows. It maps Discord response codes onto the `Disco… - [rate-limit](./src/utils/rate-limit.md) — Parses Discord's three rate-limit response headers into a single millisecond back-off duration that callers can use to… ## Installation ```bash bun add @suaveplan/discord-client ``` ## Quick Start ```ts import { DiscordBotClient, DiscordWebhook } from "@suaveplan/discord-client"; const bot = new DiscordBotClient({ token: process.env.DISCORD_BOT_TOKEN! }); await bot.sendMessage("123", { content: "hello from the bot" }); const hook = new DiscordWebhook({ webhookUrl: "https://discord.com/api/webhooks/123/abc", }); await hook.sendText("hello from a webhook"); ``` ## Modules | Module | Path | Description | |--------|------|-------------| | Bot Client | [src/core/bot-client/discord-bot-client.md](src/core/bot-client/discord-bot-client.md) | `DiscordBotClient` — send/edit/delete messages, reactions, channels, guilds | | Webhook | [src/core/webhook/discord-webhook.md](src/core/webhook/discord-webhook.md) | `DiscordWebhook` — webhook-URL delivery with structured reports | | HTTP Helper | [src/utils/http.md](src/utils/http.md) | Shared request funnel: error mapping, timeout, OTel metrics | | Rate-Limit | [src/utils/rate-limit.md](src/utils/rate-limit.md) | `parseRetryAfterMs` parser for Discord's three rate-limit headers | | Errors | [src/errors/discord-client-error.md](src/errors/discord-client-error.md) | `DISCORD_CODES` registry and `DiscordClientError` | | Types | [src/types/discord-client.types.md](src/types/discord-client.types.md) | Public type surface + Zod boundary schemas | ## Subpath Exports ```ts import { DiscordBotClient } from "@suaveplan/discord-client/bot"; import { DiscordWebhook } from "@suaveplan/discord-client/webhook"; import { DISCORD_CODES } from "@suaveplan/discord-client/errors"; import { DiscordMessageSchema } from "@suaveplan/discord-client/types"; ``` ## Error Codes | Code | HTTP | Meaning | |------|------|---------| | `DISCORD.API_ERROR` | 502 | Generic non-2xx bot response | | `DISCORD.AUTH_ERROR` | 401 | Token rejected | | `DISCORD.RATE_LIMITED` | 429 | Honour `error.context.retryAfterMs` | | `DISCORD.CHANNEL_NOT_FOUND` | 404 | Channel missing or bot lacks access | | `DISCORD.GUILD_NOT_FOUND` | 404 | Guild missing or bot is not a member | | `DISCORD.CONFIG_MISSING` | 500 | Required config value missing | | `DISCORD.INVALID_CONFIG` | 500 | Config failed Zod validation | | `DISCORD.INVALID_MESSAGE` | 400 | Message payload failed Zod validation | | `DISCORD.WEBHOOK_FAILED` | 502 | Webhook delivery rejected | | `DISCORD.TIMEOUT` | 504 | Request aborted by timeout | | `DISCORD.FORBIDDEN` | 403 | Caller lacks permissions | ## Injectable Fetch & Clock Both `DiscordBotClient` and `DiscordWebhook` accept `fetch` and `clock` injectables — pass deterministic stubs in tests, omit them in production to use `globalThis.fetch` and `Date.now`. ```ts import { DiscordBotClient, type FetchLike } from "@suaveplan/discord-client"; const stubFetch: FetchLike = async () => ({ ok: true, status: 200, statusText: "OK", headers: { get: () => null }, text: async () => "", json: async () => ({ id: "1", channelId: "1", content: "x", timestamp: "" }), }); new DiscordBotClient({ token: "abc", fetch: stubFetch, clock: () => 1_700_000_000_000, }); ``` ## OTel Metrics | Metric | Type | Labels | |--------|------|--------| | `discord_client.request.total` | Counter | `kind`, `method`, `outcome`, `status` | | `discord_client.request.duration_ms` | Histogram | `kind`, `method`, `outcome` |