Help us shape the future of Go! Take the 2026 Go Developer Survey

postmarkwebhook

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 1, 2026 License: MIT Imports: 11 Imported by: 0

README

Postmark webhooks

arkoren.dev/driver/postmark/postmarkwebhook returns a core webhook.Source authenticated with endpoint-specific HTTP Basic Auth. It needs no mail driver, API token, SDK, network lookup, or lifecycle hooks.

Install with go get arkoren.dev/driver/postmark. Follow the API reference to configure provider endpoints, bind your durable acceptance handler, and mount application-owned POST routes.

Use New(username, password) for outbound notifications and NewInbound(username, password) on a separate route for parsed inbound email. Bind Delivered, Bounced, SpamComplaint, Opened, Clicked, SubscriptionChanged, or Inbound through core webhook.Handler.

The public endpoint must use HTTPS. Configure matching Basic Auth credentials in Postmark's webhook URL. Postmark does not sign webhook bodies: Basic Auth authenticates possession of the credentials, not payload integrity independently of TLS, freshness, or Postmark origin. Add Postmark IP allowlisting at your trusted edge where possible. Do not trust arbitrary forwarded IP headers.

Outbound delivery IDs use X-PM-Webhook-Trace-Id, which Postmark documents as stable across retries. Without it, delivery IDs are empty and core deduplication is disabled. Never use MessageID alone for outbound deduplication: one message can produce multiple opens, clicks and recipient notifications. Inbound email uses its received MessageID. Use distinct application deduplication scopes for different endpoints; handlers must remain idempotent.

Authentication failures wrap webhook.ErrRejected (core defaults to 400). Malformed authenticated envelopes or bound field types return a retryable 503. Unknown outbound RecordType values are acknowledged without payload decoding. Optional or omitted fields retain their zero values. Payloads contain personal data; inbound HTML and sender claims remain untrusted.

See the Postmark webhook documentation for authentication, trace identity, retry policy, and IP ranges, and the inbound webhook documentation for the distinct inbound payload and retry policy.

Documentation

Overview

Package postmarkwebhook authenticates Postmark webhook requests using HTTP Basic Auth and provides credential-free typed notification definitions. New receives outbound notifications; NewInbound receives parsed inbound email on a separate route. Neither constructor performs network I/O or needs an API token. The application owns bindings, body limits, durable acceptance and deduplication through core/http/webhook.

HTTPS is required at the public endpoint. Basic Auth does not sign payloads or provide freshness/replay protection; protect credentials and allowlist Postmark IP ranges at a trusted edge where possible. Forward Authorization unchanged through trusted TLS-terminating proxies. Sender claims in received email remain untrusted, regardless of endpoint authentication. New and NewInbound reject empty credentials, control characters, and a colon in the username with webhook.ErrInvalid. Valid password bytes are not trimmed.

Example:

source, err := postmarkwebhook.New("webhooks", "endpoint-secret")
if err != nil {
	return err
}
handler, err := webhook.Handler(source, func(receiver *webhook.Builder[postmarkwebhook.Envelope]) {
	receiver.Handle(postmarkwebhook.Delivered, func(ctx context.Context, delivery webhook.Delivery[postmarkwebhook.Delivery]) error {
		return nil // Persist or durably enqueue before returning nil.
	})
})
if err != nil {
	return err
}
_ = handler

Index

Constants

This section is empty.

Variables

View Source
var Bounced = webhook.NewEvent("Bounce", decode[Bounce])

Bounced decodes delivery failures, including the provider's bounce category. Not every bounce makes a recipient inactive; inspect Inactive explicitly. Example: receiver.Handle(postmarkwebhook.Bounced, acceptBounce)

View Source
var Clicked = webhook.NewEvent("Click", decode[Tracking])

Clicked decodes link tracking, which can also be generated by automated scanners. Repeated clicks for one message are distinct notifications. Example: receiver.Handle(postmarkwebhook.Clicked, acceptClick)

View Source
var Delivered = webhook.NewEvent("Delivery", decode[Delivery])

Delivered decodes successful delivery notifications without credentials. Delivery means acceptance by the receiving mail server, not by a human. Example: receiver.Handle(postmarkwebhook.Delivered, acceptDelivery)

View Source
var Inbound = webhook.NewEvent("Inbound", decode[Email])

Inbound decodes received email on a source created by NewInbound. Sender addresses and content are untrusted even after endpoint authentication. Example: receiver.Handle(postmarkwebhook.Inbound, acceptEmail)

View Source
var Opened = webhook.NewEvent("Open", decode[Tracking])

Opened decodes tracking observations, not proof that a human read the email. Repeated opens for one message are distinct notifications. Example: receiver.Handle(postmarkwebhook.Opened, acceptOpen)

View Source
var SpamComplaint = webhook.NewEvent("SpamComplaint", decode[Bounce])

SpamComplaint decodes abuse reports using the same fields as Bounce. These reports can include sensitive recipient and diagnostic information. Example: receiver.Handle(postmarkwebhook.SpamComplaint, acceptComplaint)

View Source
var SubscriptionChanged = webhook.NewEvent("SubscriptionChange", decode[Subscription])

SubscriptionChanged decodes recipient suppression changes, including removals. SuppressSending determines the resulting state, not the event name alone. Example: receiver.Handle(postmarkwebhook.SubscriptionChanged, acceptChange)

Functions

func New

func New(username, password string) (webhook.Source[Envelope], error)

New authenticates outbound notifications using endpoint-specific Basic Auth. It performs no I/O. Configure these credentials on the Postmark webhook URL, not with a Postmark API token. HTTPS is mandatory at the public endpoint; TLS may terminate at a trusted proxy. Basic Auth neither signs the body nor prevents credential-holder replay. Apply IP allowlisting at the trusted edge. Missing or incorrect credentials wrap webhook.ErrRejected; malformed authenticated JSON produces a retryable processing failure. Empty credentials, control characters, or a colon in username return webhook.ErrInvalid at construction. Credentials are not trimmed or logged.

Delivery IDs use X-PM-Webhook-Trace-Id, documented as stable across retries. Without that header the ID is empty, disabling deduplication rather than incorrectly collapsing separate opens/clicks sharing a MessageID.

Example:

source, err := postmarkwebhook.New("webhooks", "endpoint-secret")
if err != nil {
	return err
}
_ = source.Err()

func NewInbound

func NewInbound(username, password string) (webhook.Source[Envelope], error)

NewInbound authenticates parsed inbound email, whose JSON has no RecordType. Use a separate route from New; every accepted envelope binds to Inbound. The same HTTPS and Basic Auth limitations as New apply. Delivery IDs use the inbound MessageID, which identifies the received email across retries. Construction applies the same credential format checks as New.

Example:

source, err := postmarkwebhook.NewInbound("inbound", "endpoint-secret")
if err != nil {
	return err
}
_ = source.Err()

Types

type Bounce

type Bounce struct {
	// Message supplies shared context; Email is the affected address here.
	// Example: _ = notification.MessageID
	Message
	// ID is the provider's numeric bounce or complaint identifier.
	// Example: _ = notification.ID
	ID int64 `json:"ID"`
	// Type is the provider category, such as HardBounce or SpamComplaint.
	// Example: _ = notification.Type
	Type string `json:"Type"`
	// Email is the affected recipient address.
	// Example: _ = notification.Email
	Email string `json:"Email"`
	// Description is the provider's explanation, not a client-safe error.
	// Example: _ = notification.Description
	Description string `json:"Description"`
	// BouncedAt is the provider's event time for bounces and complaints.
	// Example: _ = notification.BouncedAt.UTC()
	BouncedAt time.Time `json:"BouncedAt"`
	// Inactive reports whether Postmark deactivated the address.
	// Example: _ = notification.Inactive
	Inactive bool `json:"Inactive"`
}

Bounce describes a bounce or spam complaint. Extra provider fields are ignored; use Envelope with a custom event if full raw diagnostics are required. Example: _ = postmarkwebhook.Bounce{Inactive: true}

type Delivery

type Delivery struct {
	// Message supplies shared message context.
	// Example: _ = notification.MessageID
	Message
	// DeliveredAt is the provider's acceptance time, decoded from ISO 8601.
	// Example: _ = notification.DeliveredAt.UTC()
	DeliveredAt time.Time `json:"DeliveredAt"`
	// Details contains the remote mail server's response and may be sensitive.
	// Example: _ = notification.Details
	Details string `json:"Details"`
}

Delivery reports mail-server acceptance with its timestamp and SMTP response. It does not promise inbox placement or reading. Example: _ = postmarkwebhook.Delivery{}.DeliveredAt

type Email

type Email struct {
	// MessageID identifies the received email across processing retries.
	// Example: _ = notification.MessageID
	MessageID string `json:"MessageID"`
	// From is the sender's address claim, not an authenticated application user.
	// Example: _ = notification.From
	From string `json:"From"`
	// To contains the formatted recipient list.
	// Example: _ = notification.To
	To string `json:"To"`
	// Subject is untrusted message subject text.
	// Example: _ = notification.Subject
	Subject string `json:"Subject"`
	// Date retains the sender-selected timestamp format unchanged.
	// Example: _ = notification.Date
	Date string `json:"Date"`
	// TextBody is the plain text content, possibly empty.
	// Example: _ = notification.TextBody
	TextBody string `json:"TextBody"`
	// HTMLBody is unsanitized HTML; never render it without sanitization.
	// Example: _ = notification.HTMLBody
	HTMLBody string `json:"HtmlBody"`
	// StrippedTextReply is Postmark's extracted reply text when available.
	// Example: _ = notification.StrippedTextReply
	StrippedTextReply string `json:"StrippedTextReply"`
}

Email contains parsed inbound email. Date remains a string because the sending mail server chooses its format. HTML, sender claims and attachments are untrusted. Attachments and full address structures remain accessible through Envelope. Example: _ = postmarkwebhook.Email{Subject: "Support request"}

type Envelope

type Envelope struct {
	// RecordType selects the native event binding; inbound email uses "Inbound".
	// Example: _ = postmarkwebhook.Envelope{RecordType: "Open"}
	RecordType string
	// Body owns the original JSON, decoded only when the event is bound.
	// Example: _ = json.Unmarshal(envelope.Body, &notification)
	Body jsontext.Value
}

Envelope retains an authenticated Postmark notification for a bound decoder. Body contains personal data and must not be logged. Unknown fields are retained.

Example: _ = postmarkwebhook.Envelope{RecordType: "Delivery"}

type Message

type Message struct {
	// MessageID identifies the email, not an individual open or click.
	// Example: _ = notification.MessageID
	MessageID string `json:"MessageID"`
	// MessageStream is the stream configured in Postmark.
	// Example: _ = notification.MessageStream
	MessageStream string `json:"MessageStream"`
	// Recipient is the destination address for delivery and tracking events.
	// Example: _ = notification.Recipient
	Recipient string `json:"Recipient"`
	// Tag is the application's optional message label.
	// Example: _ = notification.Tag
	Tag string `json:"Tag"`
	// Metadata carries application-supplied string attributes.
	// Example: _ = notification.Metadata["order"]
	Metadata map[string]string `json:"Metadata"`
	// ServerID identifies the server when supplied by the event type.
	// Example: _ = notification.ServerID
	ServerID int64 `json:"ServerID"`
}

Message contains shared outbound message context. Fields omitted by Postmark retain zero values; MessageID is not a unique tracking-event identity. Example: _ = postmarkwebhook.Message{MessageID: "message-id"}

type Subscription

type Subscription struct {
	// Message supplies stream and recipient context.
	// Example: _ = notification.Recipient
	Message
	// ChangedAt is when the suppression changed at Postmark.
	// Example: _ = notification.ChangedAt.UTC()
	ChangedAt time.Time `json:"ChangedAt"`
	// SuppressSending is the resulting suppression state.
	// Example: _ = notification.SuppressSending
	SuppressSending bool `json:"SuppressSending"`
	// Origin identifies what initiated the change, such as Recipient.
	// Example: _ = notification.Origin
	Origin string `json:"Origin"`
	// SuppressionReason contains the provider reason, when supplied.
	// Example: _ = notification.SuppressionReason
	SuppressionReason string `json:"SuppressionReason"`
}

Subscription reports a suppression-state change. MessageID can be absent for changes not associated with a message; do not require it for routing. Example: _ = postmarkwebhook.Subscription{SuppressSending: true}

type Tracking

type Tracking struct {
	// Message supplies shared message context.
	// Example: _ = notification.MessageID
	Message
	// ReceivedAt is the provider's observation time, not local receipt time.
	// Example: _ = notification.ReceivedAt.UTC()
	ReceivedAt time.Time `json:"ReceivedAt"`
	// FirstOpen distinguishes the first recorded open for the message.
	// Example: _ = notification.FirstOpen
	FirstOpen bool `json:"FirstOpen"`
	// OriginalLink is the original clicked URL and may contain private tokens.
	// Example: _ = notification.OriginalLink
	OriginalLink string `json:"OriginalLink"`
	// ClickLocation is the provider's body location, such as HTML.
	// Example: _ = notification.ClickLocation
	ClickLocation string `json:"ClickLocation"`
	// UserAgent describes the opener or clicker and is untrusted input.
	// Example: _ = notification.UserAgent
	UserAgent string `json:"UserAgent"`
}

Tracking contains useful open and click fields. Click-only fields remain empty for opens; FirstOpen is only meaningful for open events. Example: _ = postmarkwebhook.Tracking{OriginalLink: "https://example.com"}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL