go-notificationgo-notification
Channels

Webhook (Generic)

Fire a signed HTTP POST at any URL. Glue for custom integrations.

The webhook channel POSTs a JSON payload to a URL you configure. Use it to:

  • Push notifications into a system this library doesn't have a native driver for.
  • Trigger internal services (your analytics pipeline, a Zapier/Make/n8n flow, another microservice).
  • Prototype a new driver idea before writing a proper one.

Setup

main.go
import "github.com/gopackx/go-notification/channel/webhook"

notifier.RegisterChannel(webhook.New(webhook.Config{
    DefaultURL:    os.Getenv("OPS_WEBHOOK_URL"),
    SigningSecret: os.Getenv("OPS_WEBHOOK_SECRET"), // used for HMAC signing
    DefaultHeaders: map[string]string{
        "X-Source": "go-notification",
    },
}))

The driver registers as "webhook". To run more than one webhook target, give each a distinct Config.Name and reference those names in Via.

Sending

The body is exactly what you set with SetJSON (or SetForm/SetRaw) — there is no automatic envelope. Build the payload shape your receiver expects:

main.go
func (n OrderShipped) Via(u notification.Notifiable) []string {
    return []string{"webhook"}
}

func (n OrderShipped) ToWebhook(u notification.Notifiable) *webhook.Message {
    return webhook.NewMessage().
        SetJSON(map[string]any{
            "event":      "order.shipped",
            "order_id":   n.OrderID,
            "user_id":    u.GetID(),
            "shipped_at": time.Now().UTC(),
        })
}

Per-message overrides: SetURL(url), SetMethod("PUT"), AddHeader(k, v). Use SetForm(map[string]string{...}) for form-encoded bodies or SetRaw([]byte{...}) for raw bytes.

Configuration reference

FieldTypeRequiredDescription
DefaultURLstringno*URL used when a message sets no URL. *Required unless every message calls SetURL.
DefaultHeadersmap[string]stringnoHeaders added to every request (merged with per-message).
SigningSecretstringnoHMAC-SHA256 secret. If set, adds the X-Signature header.
Timeouttime.DurationnoHTTP timeout per send. Default: 30s.
NamestringnoOverride the channel name. Default: "webhook".

The HTTP method defaults to POST and is set per message via SetMethod.

Request format

The request body is your SetJSON payload verbatim. When SigningSecret is set and the body is non-empty, the driver adds an X-Signature header:

snippet.plaintext
POST /your-webhook HTTP/1.1
Content-Type: application/json
X-Signature: sha256=<hex-hmac-sha256-of-raw-body>
X-Source: go-notification

{ "event": "order.shipped", "order_id": "A-1024", "user_id": "123", "shipped_at": "2026-04-18T10:15:03Z" }

Verifying signatures on the receiver

main.go
func verify(secret, body []byte, signatureHeader string) bool {
    mac := hmac.New(sha256.New, secret)
    mac.Write(body)
    expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(expected), []byte(signatureHeader))
}

Always compare with hmac.Equal, never == — the former is constant-time.

Non-2xx responses

The driver treats any response outside 2xx as a failed send and retries according to the notifier's retry policy. If your downstream rejects duplicates, use an idempotency key in the payload and de-dupe on the receiver.

Troubleshooting

  • Timeouts under load — increase Timeout, or make the receiver respond 202 Accepted fast and do work async.
  • Receiver sees body-less request — check that your reverse proxy (Nginx, Cloudflare) isn't stripping POST bodies on retry.
  • Signature mismatch — usually a whitespace/encoding difference on the receiver. Compare hashes with the exact raw body bytes, not a re-serialized version.