Documentation
¶
Overview ¶
Package announcer is the Go SDK for Announcer — transactional email sent from your own domain, DKIM-signed.
Create one client and reuse it; it is safe for concurrent use and holds a connection pool.
client, err := announcer.New("") // reads ANNOUNCER_API_KEY
if err != nil {
log.Fatal(err)
}
sent, err := client.Send(ctx, &announcer.SendEmailRequest{
From: "Acme <billing@acme.com>",
To: announcer.Address("customer@example.com"),
Subject: "Your receipt",
Text: "Thanks for your order.",
})
Files go in Attachments as their bytes; see Attachment.
Every send carries an Idempotency-Key, generated per call when you do not supply one, so the SDK's automatic retries can never send twice.
Index ¶
- Constants
- Variables
- type APIKey
- type APIKeysService
- type Addresses
- type Attachment
- type BatchSendResult
- type Client
- type CreatedAPIKey
- type CreatedDomain
- type CreatedWebhookEndpoint
- type DNSRecord
- type Domain
- type DomainDNS
- type DomainVerification
- type DomainsService
- func (s *DomainsService) Create(ctx context.Context, domain string) (*CreatedDomain, error)
- func (s *DomainsService) DNS(ctx context.Context, domainID string) (*DomainDNS, error)
- func (s *DomainsService) Delete(ctx context.Context, domainID string) error
- func (s *DomainsService) List(ctx context.Context) ([]Domain, error)
- func (s *DomainsService) Verify(ctx context.Context, domainID string) (*DomainVerification, error)
- type EmailsService
- func (s *EmailsService) Events(ctx context.Context, messageID string) ([]MessageEvent, error)
- func (s *EmailsService) List(ctx context.Context, opts *ListMessagesOptions) ([]Message, error)
- func (s *EmailsService) Send(ctx context.Context, req *SendEmailRequest) (*SentEmail, error)
- func (s *EmailsService) SendMany(ctx context.Context, recipients []string, msg *SendEmailRequest, ...) ([]BatchSendResult, error)
- type Error
- type ListMessagesOptions
- type Message
- type MessageEvent
- type Option
- type SendEmailRequest
- type SendManyOptions
- type SentEmail
- type Suppression
- type SuppressionsService
- type Usage
- type UsagePoint
- type VerifyOption
- type WebhookEndpoint
- type WebhookEvent
- type WebhookEventMessage
- type WebhooksService
- func (s *WebhooksService) Create(ctx context.Context, endpointURL string) (*CreatedWebhookEndpoint, error)
- func (s *WebhooksService) Delete(ctx context.Context, endpointID string) error
- func (s *WebhooksService) List(ctx context.Context) ([]WebhookEndpoint, error)
- func (s *WebhooksService) Verify(body []byte, signatureHeader, secret string, opts ...VerifyOption) (*WebhookEvent, error)
Constants ¶
const ( StatusQueued = "queued" StatusSent = "sent" StatusDelivered = "delivered" StatusBounced = "bounced" StatusComplained = "complained" StatusFailed = "failed" StatusSuppressed = "suppressed" )
Message statuses. bounced, failed and suppressed are terminal.
const ( EventSent = "sent" EventDelivered = "delivered" EventBounced = "bounced" EventComplained = "complained" EventSuppressed = "suppressed" )
Webhook event types — the transitions that produce a delivery.
const ( ScopeFull = "full" ScopeSend = "send" )
API key scopes. Give integrations ScopeSend: a leaked send key cannot register domains, mint successor keys, or reach billing.
const DefaultBaseURL = "https://mail.misralo.com"
DefaultBaseURL is where the hosted API lives.
const DefaultTolerance = 5 * time.Minute
DefaultTolerance is how far apart a delivery's timestamp and our clock may be before VerifyWebhook rejects it as a possible replay.
const Version = "0.2.1"
Version is this SDK's version, reported in the User-Agent.
Variables ¶
var ( // ErrAuthentication is a 401: the key is missing, malformed, or revoked. ErrAuthentication = newKind("authentication failed") // ErrPermission is a 403: the key is valid but not allowed to do this. // In practice a send-scoped key touching the management surface, or a // From: domain that is unregistered or unverified. ErrPermission = newKind("permission denied") // ErrNotFound is a 404: no such resource, or it belongs to another account. ErrNotFound = newKind("not found") // ErrConflict is a 409: a duplicate domain, or a send with this // Idempotency-Key still in flight. The in-flight case is retried // automatically first. ErrConflict = newKind("conflict") // ErrValidation is a 400. See Error.Errors for the per-field messages. ErrValidation = newKind("validation failed") // ErrUnprocessable is a 422: well-formed, but it cannot be carried out. ErrUnprocessable = newKind("unprocessable") // ErrSuppressedRecipient is a 422 from a send: the recipient hard-bounced // or complained before. Do not retry. ErrSuppressedRecipient = newKind("recipient is suppressed") // ErrRateLimit is a 429. See Error.RetryAfter. ErrRateLimit = newKind("rate limited") // ErrServer is a 5xx. Retried automatically before you see it. ErrServer = newKind("server error") // ErrConnection means the request never got an answer: DNS, TCP, TLS, or // the client-side timeout. Error.Unwrap also yields the underlying cause. ErrConnection = newKind("connection failed") // ErrSignatureVerification means a webhook's X-Announcer-Signature did not // check out. Treat the delivery as hostile. ErrSignatureVerification = newKind("webhook signature verification failed") )
Sentinel errors for errors.Is. Every API failure returns an *Error that unwraps to exactly one of these (a suppressed recipient unwraps to both ErrSuppressedRecipient and ErrUnprocessable), so callers can branch on the kind without type-asserting:
if errors.Is(err, announcer.ErrSuppressedRecipient) {
// they bounced or complained before; stop mailing them
}
Use errors.As to reach the details:
var apiErr *announcer.Error
if errors.As(err, &apiErr) {
log.Printf("status=%d request=%s", apiErr.StatusCode, apiErr.RequestID)
}
Functions ¶
This section is empty.
Types ¶
type APIKey ¶
type APIKey struct {
ID string `json:"id"`
Name string `json:"name"`
// Prefix is the first few characters, for telling keys apart in a list.
Prefix string `json:"prefix"`
Scope string `json:"scope"`
CreatedAt time.Time `json:"created_at"`
LastUsedAt *time.Time `json:"last_used_at"`
RevokedAt *time.Time `json:"revoked_at"`
}
APIKey is a key, minus the secret.
type APIKeysService ¶
type APIKeysService struct {
// contains filtered or unexported fields
}
APIKeysService covers issuing and revoking API keys. Needs a full-scoped key.
func (*APIKeysService) Create ¶
func (s *APIKeysService) Create(ctx context.Context, name, scope string) (*CreatedAPIKey, error)
Create issues a key. The secret is in the response and nowhere else — the API stores only its hash.
Pass ScopeSend for anything that only sends mail: a leaked send key cannot register domains, mint more keys, or reach billing. An empty scope means ScopeFull.
type Addresses ¶
type Addresses []string
Addresses is one or more email addresses.
Its underlying type is []string, so a slice literal assigns directly:
To: []string{"a@example.com", "b@example.com"},
For the common single-address case, Address reads better:
To: announcer.Address("a@example.com"),
func AddressList ¶
AddressList wraps several addresses.
type Attachment ¶ added in v0.2.0
type Attachment struct {
// Filename is the name the recipient sees, with its extension.
Filename string `json:"filename"`
// Content is the file's bytes. encoding/json writes a []byte as base64,
// which is what the API takes, so pass the bytes as they are.
Content []byte `json:"content"`
// ContentType is the media type. Guessed from Filename when empty.
ContentType string `json:"content_type,omitempty"`
// ContentID makes this an inline image: the HTML shows it as
// <img src="cid:logo"> when this is "logo".
ContentID string `json:"content_id,omitempty"`
}
Attachment is a file to attach, or an image for the HTML to show inline.
Attachments are sent and never stored: not their contents, and not their names.
type BatchSendResult ¶
type BatchSendResult struct {
To string
// Result is set when Err is nil.
Result *SentEmail
// Err is set when the send failed. Inspect it with errors.Is/errors.As.
Err error
}
BatchSendResult is one recipient's outcome from EmailsService.SendMany.
func (BatchSendResult) OK ¶
func (r BatchSendResult) OK() bool
OK reports whether this recipient's send succeeded.
type Client ¶
type Client struct {
// Emails covers sending and send history. Works with either key scope.
Emails *EmailsService
// Domains covers registering and verifying sending domains. Needs a
// full-scoped key.
Domains *DomainsService
// APIKeys covers issuing and revoking keys. Needs a full-scoped key.
APIKeys *APIKeysService
// Webhooks covers endpoints and delivery verification. Needs a
// full-scoped key for the endpoint calls.
Webhooks *WebhooksService
// Suppressions lists addresses that hard-bounced or complained.
Suppressions *SuppressionsService
// contains filtered or unexported fields
}
Client talks to the Announcer API. Build one with New.
func New ¶
New builds a client.
Pass an empty apiKey to read ANNOUNCER_API_KEY from the environment; the base URL likewise falls back to ANNOUNCER_BASE_URL and then DefaultBaseURL. It returns an error only when no key can be found.
type CreatedAPIKey ¶
type CreatedAPIKey struct {
ID string `json:"id"`
Name string `json:"name"`
Prefix string `json:"prefix"`
Scope string `json:"scope"`
// Key is the full secret. Only ever present here.
Key string `json:"key"`
}
CreatedAPIKey is a newly minted key. Key is shown once and never again — the API stores only its hash.
type CreatedDomain ¶
type CreatedDomain struct {
ID string `json:"id"`
Domain string `json:"domain"`
Selector string `json:"selector"`
Verified bool `json:"verified"`
// DNS holds every record to publish. Publish them all, then call Verify.
DNS []DNSRecord `json:"dns"`
}
CreatedDomain is a freshly registered domain, including the record to publish.
type CreatedWebhookEndpoint ¶
type CreatedWebhookEndpoint struct {
ID string `json:"id"`
URL string `json:"url"`
// Secret is the whsec_... value to pass to VerifyWebhook.
Secret string `json:"secret"`
}
CreatedWebhookEndpoint is a newly registered endpoint. Secret is shown once.
type DNSRecord ¶
type DNSRecord struct {
Type string `json:"type"`
// Name is the record name, e.g. mail._domainkey.acme.com.
Name string `json:"name"`
// Value is the record value, e.g. v=DKIM1; k=rsa; p=MIIBIjANBg...
Value string `json:"value"`
// Purpose is why the record exists, in a sentence you can show a user.
Purpose string `json:"purpose"`
}
DNSRecord is a record the domain owner has to publish.
type Domain ¶
type Domain struct {
ID string `json:"id"`
Domain string `json:"domain"`
// Selector is the DKIM selector. Always "mail"; the API ignores
// client-supplied selectors.
Selector string `json:"selector"`
// VerifiedAt is when verification first succeeded, or nil.
VerifiedAt *time.Time `json:"verified_at"`
CreatedAt time.Time `json:"created_at"`
}
Domain is a sending domain, as returned by DomainsService.List.
type DomainDNS ¶
type DomainDNS struct {
ID string `json:"id"`
Domain string `json:"domain"`
Verified bool `json:"verified"`
DNS []DNSRecord `json:"dns"`
}
DomainDNS is the record set for an existing domain, re-derived from the stored public key.
type DomainVerification ¶
DomainVerification is the outcome of a verification attempt.
type DomainsService ¶
type DomainsService struct {
// contains filtered or unexported fields
}
DomainsService covers registering and verifying sending domains. Every call here needs a full-scoped key.
func (*DomainsService) Create ¶
func (s *DomainsService) Create(ctx context.Context, domain string) (*CreatedDomain, error)
Create registers a sending domain and returns the DNS record to publish.
Registration generates an RSA keypair, so the API rate-limits it hard — roughly one a minute. Publish every record in CreatedDomain.DNS, then call Verify. A domain that is already registered fails with ErrConflict.
func (*DomainsService) DNS ¶
DNS returns the records for a domain, re-derived from the stored public key — so "what was I supposed to publish?" stays answerable after registration.
func (*DomainsService) Delete ¶
func (s *DomainsService) Delete(ctx context.Context, domainID string) error
Delete removes the domain and its signing key. Send history survives; sending from the domain stops immediately.
func (*DomainsService) List ¶
func (s *DomainsService) List(ctx context.Context) ([]Domain, error)
List returns every domain on the account, newest first.
func (*DomainsService) Verify ¶
func (s *DomainsService) Verify(ctx context.Context, domainID string) (*DomainVerification, error)
Verify resolves the DKIM record and compares it to the key we issued.
This is a real DNS lookup, not a self-report: it only succeeds on an actual match. A missing or mismatched record fails with ErrUnprocessable. DNS propagation takes minutes to hours — retry rather than re-registering.
type EmailsService ¶
type EmailsService struct {
// contains filtered or unexported fields
}
EmailsService covers sending mail and reading send history. Both work with either key scope.
func (*EmailsService) Events ¶
func (s *EmailsService) Events(ctx context.Context, messageID string) ([]MessageEvent, error)
Events returns the audit trail for one message: every status transition, oldest first.
func (*EmailsService) List ¶
func (s *EmailsService) List(ctx context.Context, opts *ListMessagesOptions) ([]Message, error)
List returns send history, newest first. A nil opts asks for the API's defaults.
func (*EmailsService) Send ¶
func (s *EmailsService) Send(ctx context.Context, req *SendEmailRequest) (*SentEmail, error)
Send sends one email.
sent, err := client.Emails.Send(ctx, &announcer.SendEmailRequest{
From: "Acme <billing@acme.com>",
To: []string{"customer@example.com", "partner@example.com"},
Cc: announcer.Address("accounting@acme.com"),
ReplyTo: announcer.Address("support@acme.com"),
Subject: "Your receipt",
Text: "Thanks!",
})
To and Cc go out as one email whose recipients see each other; Bcc recipients see nobody. At most 50 addresses across the three.
Attachments take the file's bytes:
pdf, _ := os.ReadFile("invoice.pdf")
sent, err := client.Emails.Send(ctx, &announcer.SendEmailRequest{
From: "billing@acme.com",
To: announcer.Address("customer@example.com"),
Subject: "Invoice 1042",
Text: "Your invoice is attached.",
Attachments: []announcer.Attachment{{Filename: "invoice-1042.pdf", Content: pdf}},
})
An Idempotency-Key is generated when SendEmailRequest.IdempotencyKey is empty, so the SDK's automatic retries can never send twice. Set it yourself — an order id, a job id — to extend that guarantee across process restarts.
A recipient on the account's suppression list is dropped and reported in SentEmail.Suppressed; the rest of the message still goes out. Only when every recipient is suppressed does this return an error.
Failures worth branching on: ErrPermission (the From domain is not registered or not verified), ErrSuppressedRecipient (every recipient bounced or complained before), ErrRateLimit (a limit or a quota; see Error.RetryAfter — quota counts recipients, so one call can consume several).
func (*EmailsService) SendMany ¶
func (s *EmailsService) SendMany( ctx context.Context, recipients []string, msg *SendEmailRequest, opts *SendManyOptions, ) ([]BatchSendResult, error)
SendMany sends the same message to several recipients as separate emails, one API call each, and returns a result per recipient in the order given.
This is not the same as putting several addresses in SendEmailRequest.To:
- To: []string{a, b} is one email. A and B see each other in the To header, it costs one request, and one bounce marks one message.
- SendMany([]string{a, b}, …) is two emails. Neither knows the other exists, each gets its own idempotency key and its own bounce, and one failure leaves the other untouched.
Use this one for anything list-shaped — a newsletter, a digest, a fan-out. A Cc on msg is copied on every message, so a three-recipient batch sends the Cc three copies; that is usually not what you want.
The returned error is non-nil only when the context was cancelled; the results gathered up to that point are still returned. Per-recipient failures live in BatchSendResult.Err.
type Error ¶
type Error struct {
// StatusCode is the HTTP status, or 0 when the request never reached the API.
StatusCode int
// Message is the most useful sentence the API gave us.
Message string
// Title and Detail come from the RFC 9457 problem body, when there was one.
Title string
Detail string
// Errors holds per-field messages on a 400. Field names are verbatim.
Errors map[string][]string
// RetryAfter is set on a 429, from the Retry-After header.
RetryAfter time.Duration
// Recipient is set when a send was refused because the address is
// suppressed. It is the first entry of Suppressed when the API named any,
// and otherwise the address the SDK sent.
Recipient string
// Suppressed holds every address the API refused, from the RFC 9457
// extension member on a suppression refusal. Prefer it over parsing
// Message: it is there precisely so clients need not read the prose.
Suppressed []string
// RequestID is the response's x-request-id. Quote it in support.
RequestID string
// Raw is the error body exactly as the API sent it, for anything this
// struct does not model.
Raw json.RawMessage
// contains filtered or unexported fields
}
Error is the single error type returned for every API failure.
type ListMessagesOptions ¶
type ListMessagesOptions struct {
// Limit is clamped to 1-200 by the API. Zero means the API default of 50.
Limit int
// Status filters by one of the Status* constants.
Status string
// Search matches a substring of the recipient address.
Search string
}
ListMessagesOptions filters send history. Zero values are omitted.
type Message ¶
type Message struct {
ID string `json:"id"`
MessageID *string `json:"message_id"`
From string `json:"header_from"`
// To is the primary recipient — the first To address. A message with Cc,
// Bcc or several To addresses reports its first here and the total in
// RecipientCount.
To string `json:"recipient"`
// RecipientCount is how many addresses the message went to, across To, Cc
// and Bcc.
RecipientCount int `json:"recipient_count"`
// AttachmentCount is how many files were attached. Their names and
// contents are not kept.
AttachmentCount int `json:"attachment_count"`
// ReplyTo is the Reply-To header that went out, if any.
ReplyTo *string `json:"reply_to"`
Subject *string `json:"subject"`
// Status is the rolled-up status. One bounced recipient makes the whole
// message bounced — it is the thing you have to act on.
Status string `json:"status"`
CreatedAt time.Time `json:"created_at"`
}
Message is one row of send history.
The json tags are the API's own names; the Go field names are the ones the rest of this package uses. header_from and recipient are the database's vocabulary, while the webhook payload already says from/to — one vocabulary is worth the mapping.
type MessageEvent ¶
type MessageEvent struct {
ID int64 `json:"id"`
Event string `json:"event"`
// Payload is event-specific data, left exactly as the API sent it — these
// keys are tenant data.
Payload map[string]any `json:"payload"`
CreatedAt time.Time `json:"created_at"`
}
MessageEvent is one entry in a message's audit trail.
type Option ¶
type Option func(*Client)
Option configures a Client.
func WithBaseURL ¶
WithBaseURL points the client at a different API root, such as a mock server in tests.
func WithHTTPClient ¶
WithHTTPClient supplies your own *http.Client (proxies, custom TLS, instrumentation). Its Timeout, if set, applies per attempt.
func WithHeader ¶
WithHeader adds a header to every request.
func WithMaxRetries ¶
WithMaxRetries sets how many extra attempts follow a retryable failure. Default 2, so three attempts in all. Zero disables retries.
func WithTimeout ¶
WithTimeout sets the per-attempt timeout. Default 30s.
func WithUserAgent ¶
WithUserAgent appends your application's name to the SDK's own User-Agent.
type SendEmailRequest ¶
type SendEmailRequest struct {
// From is the sender. Its domain must be registered to this account.
From string `json:"from"`
// To holds the primary recipients. At least one is required.
To Addresses `json:"to"`
// Cc holds carbon copies, visible to every other recipient.
Cc Addresses `json:"cc,omitempty"`
// Bcc holds blind copies. They receive the message; nobody — including
// the other blind copies — sees that they did.
Bcc Addresses `json:"bcc,omitempty"`
// ReplyTo is where replies should go. A header only: no delivery, nothing
// billable, nothing that can bounce.
ReplyTo Addresses `json:"reply_to,omitempty"`
Subject string `json:"subject,omitempty"`
// Text is the plain-text body. Supply it even alongside HTML — filters
// like seeing both.
Text string `json:"text,omitempty"`
// HTML is the HTML body.
HTML string `json:"html,omitempty"`
// Attachments are files to attach, or images for the HTML to show
// inline. At most 20, and 10 MB per message in all, Text and HTML
// included. Executables (.exe, .js, .bat, ...) are refused, because Gmail
// and most mail servers would refuse them anyway.
Attachments []Attachment `json:"attachments,omitempty"`
// IdempotencyKey makes the send exactly-once. Left empty, the SDK
// generates one per call so its own retries cannot double-send; set it
// yourself (an order id, a job id) to keep that guarantee across process
// restarts. Not serialised — it travels as a header.
IdempotencyKey string `json:"-"`
}
SendEmailRequest is one email to send.
Every address accepts either a bare address (billing@acme.com) or a display name (Acme Billing <billing@acme.com>). At least one of Text or HTML is required, and at most 50 addresses across To, Cc and Bcc combined.
To and Cc go out as one email whose recipients see each other; Bcc recipients see nobody. For separate emails that share nothing, use EmailsService.SendMany.
type SendManyOptions ¶
type SendManyOptions struct {
// StopOnError abandons the remaining recipients after the first failure.
// Off by default: one suppressed address should not sink a batch.
StopOnError bool
}
SendManyOptions tunes SendMany.
type SentEmail ¶
type SentEmail struct {
// ID is Announcer's id for the message. Use it with EmailsService.Events.
ID string `json:"id"`
// MessageID is the RFC 5322 Message-ID the MTA assigned.
MessageID *string `json:"messageId"`
Status string `json:"status"`
// IdempotentReplay is true when this key had already been used: nothing
// was sent a second time and these are the original send's details.
IdempotentReplay bool `json:"idempotentReplay"`
// Recipients is how many addresses the message went to, across To, Cc and
// Bcc. This is the number billed and counted against quota.
Recipients int `json:"recipients"`
// Suppressed holds addresses dropped because they are on the account's
// suppression list. Empty on a clean send — the rest of the message still
// went out. Only when every recipient is suppressed does the send fail.
Suppressed []string `json:"suppressed"`
}
SentEmail is the result of a successful send.
type Suppression ¶
type Suppression struct {
ID string `json:"id"`
Email string `json:"email"`
// Reason is why it was suppressed, e.g. "hard bounce (5.1.1 user unknown)".
Reason string `json:"reason"`
CreatedAt time.Time `json:"created_at"`
}
Suppression is an address Announcer refuses to send to.
type SuppressionsService ¶
type SuppressionsService struct {
// contains filtered or unexported fields
}
SuppressionsService lists addresses that hard-bounced or complained. Works with either key scope.
func (*SuppressionsService) List ¶
func (s *SuppressionsService) List(ctx context.Context, limit int) ([]Suppression, error)
List returns addresses Announcer refuses to send to, newest first. A limit of zero asks for the API's default of 50.
type Usage ¶
type Usage struct {
SentToday int64 `json:"sentToday"`
DailySendLimit int `json:"dailySendLimit"`
Domains int64 `json:"domains"`
MaxDomains int `json:"maxDomains"`
Plan string `json:"plan"`
SentThisPeriod int64 `json:"sentThisPeriod"`
PeriodStart time.Time `json:"periodStart"`
// MonthlyIncludedMessages is nil on plans with no monthly accounting.
MonthlyIncludedMessages *int `json:"monthlyIncludedMessages"`
// MonthlyHardCap is the spend ceiling. Nil when uncapped.
MonthlyHardCap *int `json:"monthlyHardCap"`
// OverageMinorUnits is what the current period's overage would cost.
OverageMinorUnits *int64 `json:"overageMinorUnits"`
// Series is the last 14 days, oldest first. Days with no sends are
// present as zeroes rather than missing.
Series []UsagePoint `json:"series"`
}
Usage is consumption against this account's limits.
type UsagePoint ¶
type UsagePoint struct {
// Date is YYYY-MM-DD.
Date string `json:"date"`
Sent int64 `json:"sent"`
Delivered int64 `json:"delivered"`
// Failed is bounced + complained + failed. A subset of Sent, not
// additional to it.
Failed int64 `json:"failed"`
}
UsagePoint is one day of the 14-day sending series.
type VerifyOption ¶
type VerifyOption func(*verifyConfig)
VerifyOption tunes VerifyWebhook.
func WithClock ¶
func WithClock(now time.Time) VerifyOption
WithClock fixes the current time. For tests.
func WithTolerance ¶
func WithTolerance(d time.Duration) VerifyOption
WithTolerance sets the clock-skew allowance. Zero disables the timestamp check entirely, which also disables replay protection — do that only if something else in your stack already provides it.
type WebhookEndpoint ¶
type WebhookEndpoint struct {
ID string `json:"id"`
URL string `json:"url"`
CreatedAt time.Time `json:"created_at"`
DisabledAt *time.Time `json:"disabled_at"`
}
WebhookEndpoint is a registered endpoint.
func (WebhookEndpoint) Disabled ¶
func (w WebhookEndpoint) Disabled() bool
Disabled reports whether the endpoint has been disabled.
type WebhookEvent ¶
type WebhookEvent struct {
// Event is one of the Event* constants.
Event string `json:"event"`
OccurredAt time.Time `json:"occurredAt"`
Message WebhookEventMessage `json:"message"`
// Detail is event-specific data, e.g. {"dsnStatus": "5.1.1 ..."} on a bounce.
Detail map[string]any `json:"detail"`
}
WebhookEvent is a verified webhook delivery.
func VerifyWebhook ¶
func VerifyWebhook(body []byte, signatureHeader, secret string, opts ...VerifyOption) (*WebhookEvent, error)
VerifyWebhook checks a webhook delivery and returns the parsed event.
Pass the raw request body — the exact bytes Announcer sent. Re-serialising a parsed struct reorders keys and changes whitespace, and the signature will not match:
func handler(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "", http.StatusBadRequest)
return
}
event, err := announcer.VerifyWebhook(
body,
r.Header.Get("X-Announcer-Signature"),
os.Getenv("ANNOUNCER_WEBHOOK_SECRET"),
)
if err != nil {
http.Error(w, "", http.StatusBadRequest)
return
}
...
}
Errors match ErrSignatureVerification. The timestamp is inside the MAC, so the staleness check is what actually stops a captured delivery being replayed at you later.
type WebhookEventMessage ¶
type WebhookEventMessage struct {
ID string `json:"id"`
From string `json:"from"`
To string `json:"to"`
Subject *string `json:"subject"`
Status string `json:"status"`
}
WebhookEventMessage is the message summary carried on a webhook delivery.
type WebhooksService ¶
type WebhooksService struct {
// contains filtered or unexported fields
}
WebhooksService covers webhook endpoints. Registering needs a full-scoped key; verifying a delivery needs nothing but the secret.
func (*WebhooksService) Create ¶
func (s *WebhooksService) Create(ctx context.Context, endpointURL string) (*CreatedWebhookEndpoint, error)
Create registers an endpoint. Maximum two active per account.
The returned Secret crosses the wire exactly once — store it now and pass it to VerifyWebhook on every delivery.
func (*WebhooksService) Delete ¶
func (s *WebhooksService) Delete(ctx context.Context, endpointID string) error
Delete disables an endpoint. Pending deliveries stop; history stays auditable.
func (*WebhooksService) List ¶
func (s *WebhooksService) List(ctx context.Context) ([]WebhookEndpoint, error)
List returns every endpoint on the account, including disabled ones.
func (*WebhooksService) Verify ¶
func (s *WebhooksService) Verify(body []byte, signatureHeader, secret string, opts ...VerifyOption) (*WebhookEvent, error)
Verify checks a delivery's signature and returns the parsed event. It is a method for discoverability; VerifyWebhook is the same function and needs no client.