Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Webhook sink

Posts a JSON envelope to a configurable HTTP endpoint. Use this when the downstream consumer is another service rather than a person — internal scoreboards, downstream workers, audit sinks.

import (
    "github.com/frankbardon/parsec"
    "github.com/frankbardon/parsec/sinks/webhook"
)

w, _ := webhook.New(webhook.Config{
    URL: "https://hooks.internal.example.com/parsec",
    Headers: map[string]string{
        "X-Parsec-Source": "prod",
        "Authorization":   "Bearer " + os.Getenv("DOWNSTREAM_TOKEN"),
    },
})

p, _ := parsec.New(parsec.Options{})
p.Sinks().Register(w)

Then:

_, _ = p.PublishOrSink(ctx,
    "private:webapp.user.42.downloads",
    []byte(`{"status":"ready"}`),
    "webhook",
    webhook.Recipient{URL: ""}, // use the configured URL
    parsec.Message{
        Subject: "download.ready",
        Body:    `{"user_id":42}`,
        Metadata: map[string]string{"trace_id": "abc"},
    })

Configuration

webhook.Config:

FieldRequiredNotes
URLyesDefault POST target. Used when the recipient does not override it.
HeadersnoStatic headers added to every request. Useful for an auth header or a fixed X-Parsec-Source.
HTTPClientnoDefaults to http.DefaultClient. Inject your own for timeouts, tracing, retries.

Recipient

webhook.Recipient:

type Recipient struct {
    URL string
}

URL == "" (or passing nil / a non-webhook.Recipient value) means “use the configured URL”. A non-empty URL overrides the destination on a per-call basis — useful when one sink instance fans out to multiple downstream services that share a common header set.

Payload shape

The sink marshals a fixed envelope:

{
  "subject":  "download.ready",
  "body":     "{\"user_id\":42}",
  "metadata": {"trace_id": "abc"}
}

Fields with zero values are omitted. The wire Content-Type is application/json. Configured headers are set after the Content-Type, so a config that overrides Content-Type wins.

Failure semantics

ConditionReturned code
Missing URL at constructionPARSEC_SINK_CONFIG_INVALID
Marshalling the envelope failsPARSEC_INTERNAL (very rare)
http.Client.Do returns an errorPARSEC_SINK_UNAVAILABLE
Downstream returns >= 300 statusPARSEC_SINK_UNAVAILABLE (carries the status line)

The sink does NOT retry, NOT queue, and NOT batch. It’s a thin forwarder. If downstream is flaky, wrap it.

Patterns

  • One sink per downstream service is the easy default — name them "webhook-audit", "webhook-scoreboard", etc.
  • One sink + per-call URL override works when the destinations share headers (auth, tracing) but differ in path.
  • For at-least-once semantics, queue the message yourself (e.g. in Kafka or a database table) and have a worker call Send with a retry loop. Parsec sinks are best-effort, not durable.

See also