Using the Shoutrrr Package¶
Overview¶
The Shoutrrr Go package (github.com/nicholas-fedor/shoutrrr) enables sending notifications to various services (e.g., discord, slack, telegram, smtp, etc.) using service URLs. It provides two primary methods: a direct Send function for simple use cases and a Sender struct for advanced scenarios with multiple URLs, message queuing, and parameter customization.
Minimum Supported Go Version¶
Projects importing Shoutrrr are expected to follow the latest Go minor and/or patch semantic versions; ergo, Shoutrrr follows the latest minor Go version, i.e. 1.27. (Go refers to minor semantic version releases as major releases.)
Go release information can be reviewed here: https://go.dev/doc/devel/release
Usage¶
import "github.com/nicholas-fedor/shoutrrr"
Direct Send¶
Sends a notification to a single service URL.
- Functions:
shoutrrr.Send(url string, message string) errorshoutrrr.SendContext(ctx context.Context, url string, message string, params *types.Params) error- Behavior: Initializes a service from the provided URL, sends the message within the service's send budget, closes
the service, and returns any error. A send failure wraps a
*types.TargetError, which names the service but never includes the URL, so it is safe to log. Useerrors.Asto get it. An invalid or unsupported URL returns an ordinary wrapped error instead. - Parameters:
Sendtakes no parameters. UseSendContextto pass parameters such as a title, and to cancel the send or bound it with a deadline.paramsmay benil.
Example
url := "slack://hook:T00000000-B00000000-XXXXXXXXXXXXXXXXXXXXXXXX@webhook"
err := shoutrrr.Send(url, "Hello, Slack!")
if err != nil {
fmt.Println("Error:", err)
}
Example
import (
"context"
"fmt"
"time"
"github.com/nicholas-fedor/shoutrrr"
"github.com/nicholas-fedor/shoutrrr/pkg/types"
)
func notify(url string) error {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
params := types.Params{}
params.SetTitle("Alert")
if err := shoutrrr.SendContext(ctx, url, "System alert!", ¶ms); err != nil {
return fmt.Errorf("sending alert: %w", err)
}
return nil
}
Sender¶
Creates a Sender (*ServiceRouter) to manage multiple service URLs, support message queuing, and allow parameter customization.
- Function:
shoutrrr.CreateSenderWithOptions(opts types.SenderOptions, urls ...string) (*ServiceRouter, error).shoutrrr.NewSenderWithOptionsalso takes a logger.CreateSenderandNewSenderare deprecated. - Options:
types.SenderOptions{}uses the defaults.HTTPClientis injected into HTTP services.DialContextis injected into TCP services that implementtypes.DialContextSetter.shoutrrr.Sendandshoutrrr.SendContextcannot take these options. - Methods:
Send(message string, params *types.Params) []error: Sends a message to all configured services.SendContext(ctx context.Context, message string, params *types.Params) []error: LikeSend, and returns whenctxis canceled or its deadline passes.SendItems(items []types.MessageItem, params types.Params) []error: Sends structured message items to services that support rich formatting.SendItemsContext(ctx context.Context, items []types.MessageItem, params types.Params) []error: LikeSendItems, bounded byctx.SendAsync(message string, params *types.Params) chan error: Sends a message asynchronously and returns a channel of errors.Enqueue(message string, v ...any): Queues a formatted message for later sending.Flush(params *types.Params): Sends all queued messages and resets the queue.Close() error: Closes services that hold resources between sends, such as MQTT connections.- Behavior: Initializes a service for each URL and sends to all of them concurrently.
Send,SendContext,SendItems, andSendItemsContextreturn one entry per configured URL, in the same order, and the entry isnilwhen that send succeeded.
Example
urls := []string{
"slack://hook:T00000000-B00000000-XXXXXXXXXXXXXXXXXXXXXXXX@webhook",
"telegram://110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw@telegram?channels=@mychannel",
}
sender, err := shoutrrr.CreateSenderWithOptions(types.SenderOptions{}, urls...)
if err != nil {
log.Fatal(err)
}
defer sender.Close()
params := types.Params{}
params.SetTitle("Test Notification")
for i, err := range sender.Send("Hello, world!", ¶ms) {
if err != nil {
fmt.Printf("Error sending to URL %d: %v\n", i, err)
}
}
Message Queuing¶
Allows queuing messages for deferred sending, useful for aggregating notifications during a process.
- Queues messages with
Enqueueand sends them withFlush. Queued messages use theparamsprovided duringFlush.
Example
url := "discord://abc123@123456789"
sender, err := shoutrrr.CreateSenderWithOptions(types.SenderOptions{}, url)
if err != nil {
log.Fatal(err)
}
defer sender.Flush(nil)
sender.Enqueue("Started doing work")
if err := doWork(); err != nil {
sender.Enqueue("Error: %v", err)
return
}
sender.Enqueue("Work completed successfully!")
SendItems and RichSender¶
Sends structured message items to services that support rich formatting.
Services implementing types.RichSender receive the full []types.MessageItem slice, preserving level, fields, and file attachments.
Services that do not implement RichSender fall back to plain text automatically.
- Method:
SendItems(items []types.MessageItem, params types.Params) []error - Behavior: Dispatches to
ContextAttachmentSenderif available, otherwiseRichSender, otherwise falls back toSendwith plain text.
Example
items := []types.MessageItem{
{Text: "Deployment complete", Level: types.Info},
{Text: "Rollback available", Level: types.Warning},
}
sender, err := shoutrrr.CreateSenderWithOptions(types.SenderOptions{}, "discord://TOKEN@WEBHOOK_ID")
if err != nil {
log.Fatal(err)
}
errs := sender.SendItems(items, types.Params{})
Context Propagation¶
shoutrrr.SendContext, ServiceRouter.SendContext, and ServiceRouter.SendItemsContext take a caller context.
Services that implement types.ContextSender or types.ContextAttachmentSender receive a context derived from it with
the service's send budget, so cancellation and deadlines stop their requests. For other services, the call returns when
the context ends, and the service's error reports the context's error. Send and SendItems use a background context.
Per-Target Errors¶
*ServiceRouter.Send, SendContext, SendItems, and SendItemsContext return one entry per configured URL, in the
order the URLs were given, and the entry is nil when that send succeeded. SendAsync's channel receives one result per
service, nil for a successful send, in the order the sends finish, and then closes. Route and shoutrrr.SendContext return a single error. A failed send is a *types.TargetError, or wraps
one, so use errors.As to get it. Its URL field holds the service ID (such as discord) and its Index field holds
the URL's position. It never contains the service URL, so it is safe to log, and it supports errors.Unwrap and
errors.Is. Creating a sender and locating a service return ordinary wrapped errors, not a *types.TargetError.
Example
errs := sender.Send("deploy complete", nil)
for i, err := range errs {
if err == nil {
continue
}
var targetErr *types.TargetError
if errors.As(err, &targetErr) {
log.Printf("failed to send to URL %d (%s): %v", i, targetErr.URL, targetErr.Err)
}
}
Logging¶
Services log what they do, such as a warning that a send skips TLS certificate verification.
Every logger in the API is a types.StdLogger, which *log.Logger satisfies.
shoutrrr.SetLogger sets the logger for Send and SendContext.
shoutrrr.NewSenderWithOptions, router.NewWithOptions, and ServiceRouter.SetLogger set the logger for a sender.
A nil logger discards everything.
Services log leveled records.
Each one has a level, a constant message, and key/value attributes.
A sender adds the service's ID as the service attribute.
To keep the levels, pass a *slog.Logger through types.NewSlogLogger.
Its handler then receives each record, so it filters and formats them.
Example
handler := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelWarn})
shoutrrr.SetLogger(types.NewSlogLogger(slog.New(handler)))
A plain logger receives each record as one line, in the format of slog's default handler. It receives every level, because it cannot filter them.
WARN sending without TLS service=ntfy
slog.NewLogLogger also turns a slog handler into a plain logger, but it logs every line at the one level it is given.
Use types.NewSlogLogger to keep each record's level.
The logger service writes each notification as text, which reaches a plain logger unchanged.
A slog logger receives it as a record at INFO.
Message Levels¶
Services that support severity or priority can receive a semantic level through the level param.
Shoutrrr defines five levels: Unknown, Debug, Info, Warning, and Error.
The default is Info.
Example
params := types.Params{}
params.SetLevel(types.Warning)
params.SetTitle("Disk usage high")
sender, err := shoutrrr.CreateSenderWithOptions(types.SenderOptions{}, "discord://TOKEN@WEBHOOK_ID")
if err != nil {
log.Fatal(err)
}
errs := sender.Send("Root partition at 92%", ¶ms)
Format Conversion¶
Converts message bodies between supported formats.
import "github.com/nicholas-fedor/shoutrrr/pkg/format"
body, err := format.ConvertFormat("Hello **world**", "markdown", "text")
Supported conversions: text ↔ markdown ↔ html.
Service Discovery¶
Enumerates or checks available notification services without constructing a router.
import "github.com/nicholas-fedor/shoutrrr/pkg/services"
for _, schema := range services.SupportedSchemas() {
fmt.Println(schema)
}
if services.SupportsSchema("discord") {
// ...
}
Examples¶
Send with Parameters and Error Handling¶
Example
url := "discord://abc123@123456789"
params := types.Params{}
params.SetTitle("Alert")
if err := shoutrrr.SendContext(context.Background(), url, "System alert!", ¶ms); err != nil {
fmt.Println("Error:", err)
}
(No output on success)
Error: sending message: discord: <the service's error>
Send to Multiple Services with Queuing¶
Example
urls := []string{
"slack://hook:T00000000-B00000000-XXXXXXXXXXXXXXXXXXXXXXXX@webhook",
"discord://abc123@123456789",
}
sender, err := shoutrrr.CreateSenderWithOptions(types.SenderOptions{}, urls...)
if err != nil {
log.Fatal(err)
}
start := time.Now()
params := types.Params{}
params.SetTitle("Task Summary")
defer sender.Flush(¶ms)
sender.Enqueue("Task started")
time.Sleep(time.Second)
sender.Enqueue("Task finished in %v", time.Now().Sub(start))
(No output on success)
2026/10/10 12:00:00 creating sender: error initializing router services: URL 0: slack: invalid slack token format
Notes¶
- Error Handling:
shoutrrr.Sendandshoutrrr.SendContextreturn a single error.Sender.Send,SendContext,SendItems, andSendItemsContextreturn one entry per URL,nilon success, so check each entry rather than the length of the slice. A failed send is or wraps a*types.TargetErrornaming the service, never its URL, so useerrors.Asto get it. Creating a sender and locating a service return ordinary wrapped errors. - Uninitialized Services: A service created directly from its package, without the router, returns
types.ErrServiceNotInitializedfrom every send made beforeInitializeruns, so useerrors.Isto check for it. - Parameters:
paramsis a*types.Paramsvalue forSend,SendContext,SendAsync, andFlush, includingshoutrrr.SendContext.SendItemsacceptstypes.Paramsby value. Use setter methods such asSetTitle,SetMessage, andSetLevelto configure service-specific options. Useshoutrrr docsto view supported parameters for each service. - Timeouts: The default is 10 seconds per service. A longer service timeout extends that service. A positive
SenderOptions.Timeoutis the exact fixed timeout for every service. - Duplicate URLs: A
Sendersends to every URL it is given, including duplicates. The CLI'ssendcommand removes duplicate URLs before sending.