Hex.pm Hex Docs CI License: MIT

Discord REST API v10 client for Elixir (https://discord.com/api/v10).

Req-based, bot-token auth, bounded rate-limit-aware retry, lazy pagination streams, embed builders with limit validation, GFM -> Discord markdown conversion with 2000-character chunking, webhooks, and Ed25519 interaction signature verification.

REST-only. There is no gateway/websocket client: this library sends requests but does not receive gateway events (messages, presence, voice). Like the other house SDKs it stays stateless and process-free. Receive slash commands and component clicks through HTTP interactions (Noizu.Discord.Interactions).

Installation

def deps do
  [
    {:noizu_discord, "~> 0.1.0"}
  ]
end

Runtime dependencies: Req and Jason. No supervision tree.

Configuration

Token precedence: Client.new(token: ...) -> app env -> DISCORD_BOT_TOKEN.

# config/runtime.exs (optional)
config :noizu_discord, :client,
  token: System.get_env("DISCORD_BOT_TOKEN")
Environment variableUsed for
DISCORD_BOT_TOKENBot token (sent as Authorization: Bot <token>); fallback when neither opts nor app env set one
Client optionDefault
:tokensee precedence above
:base_urlhttps://discord.com/api/v10
:user_agentDiscordBot (https://github.com/noizu-labs-sdks/elixir-discord, 0.1.0)
:req_options[] (merged into every Req.request/1, e.g. plug: for tests)
:retrymax_attempts: 3, base_backoff_ms: 500, max_delay_ms: 30_000

Usage

client = Noizu.Discord.client()

{:ok, me} = Noizu.Discord.Users.me(client)
{:ok, guilds} = Noizu.Discord.Guilds.me(client)

alias Noizu.Discord.{Channels, Embeds, Markdown}

embed =
  Embeds.new()
  |> Embeds.title("Deploy finished")
  |> Embeds.description(Markdown.from_markdown(notes, target: :embed))
  |> Embeds.color("#2ecc71")
  |> Embeds.field("Env", "prod", true)

{:ok, [embed]} = Embeds.validate(embed, on_overflow: :truncate)

{:ok, msg} = Channels.create_message(client, channel_id, %{content: "Done", embeds: [embed]})
{:ok, _} = Noizu.Discord.Reactions.create(client, channel_id, msg["id"], "✅")

# Long markdown in message content: convert, then split at 2000 chars
for part <- report |> Markdown.from_markdown() |> Markdown.chunk() do
  Channels.create_message(client, channel_id, %{content: part})
end

# Lazy history (newest first, `before` paging)
client |> Channels.messages_stream(channel_id) |> Enum.take(500)

# Webhooks need no bot token
Noizu.Discord.Webhooks.execute(webhook_url, %{content: "hi", username: "ci"})

Results, errors, rate limits

Every call returns {:ok, body} (nil for 204) or {:error, %Noizu.Discord.Error{}} — never raises. Discord's {code, message, errors} envelope becomes tag: :api, reason: <code>; see Noizu.Discord.Error for all tags.

429s are retried (up to 3 attempts, also for POST — Discord does not apply rate-limited requests) honouring retry_after / X-RateLimit-Reset-After; 5xx are retried for idempotent methods only. Pass meta: true to get {:ok, body, %{status, attempts, rate_limit: %{bucket, limit, remaining, reset, reset_after, scope, global}}}.

Interactions

if Noizu.Discord.Interactions.verify_signature(public_key_hex, sig_hex, timestamp, raw_body) do
  Noizu.Discord.Interactions.respond(interaction["id"], interaction["token"], {:message, %{content: "pong"}})
end

Not covered in 0.1.0: file uploads (multipart), application command registration, gateway.

Development

mix deps.get
mix format --check-formatted
mix compile --warnings-as-errors
mix test        # Req.Test plug stubs only — no network
mix docs

License

MIT — see LICENSE.