whatsapp

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Jun 17, 2026 License: MIT Imports: 17 Imported by: 0

README

gowhatsapp

Go Reference Go Report Card Go Version CI GitHub tag codecov

A clean, dependency-free Go client for the WhatsApp Cloud API (Meta Graph API). Send text, media, location, reactions, and approved templates (including authentication templates for OTP delivery), and receive webhooks (delivery statuses + inbound messages) with signature verification.

  • Zero third-party dependencies — standard library only.
  • Targets Meta Cloud API directly — not a BSP wrapper.
  • Typed, matchable errorserrors.Is + *APIError with fbtrace_id.
  • Hermetic tests — a built-in MockTransport captures exact payloads.
  • Future-proof — pinned/overridable API version, swappable transport, sealed message types. See DESIGN.md.

OTP note: Meta only delivers the code; it does not generate or verify it. Keep generation + verification in your app. AuthTemplate formats the delivery.

Installation

go get github.com/KARTIKrocks/gowhatsapp

Requires Go 1.22+.

Quick start

package main

import (
	"context"
	"log"

	whatsapp "github.com/KARTIKrocks/gowhatsapp"
)

func main() {
	client, err := whatsapp.New(whatsapp.Config{
		PhoneNumberID: "1234567890",   // Cloud API phone number ID
		AccessToken:   "EAAG...",      // system-user access token
	})
	if err != nil {
		log.Fatal(err)
	}

	ctx := context.Background()

	// Free-form text — only inside an open 24-hour service window.
	if _, err := client.SendText(ctx, "15551234567", "Hello from gowhatsapp!"); err != nil {
		log.Fatal(err)
	}

	// Approved template — to start a conversation or message outside the window.
	res, err := client.SendTemplate(ctx, "15551234567",
		whatsapp.AuthTemplate("otp_login", "en_US", "472913"))
	if err != nil {
		log.Fatal(err)
	}
	log.Printf("queued message %s", res.MessageID)
}

Messages

client.Send(ctx, to, whatsapp.Text{Body: "hi", PreviewURL: true})
client.Send(ctx, to, whatsapp.ImageByLink("https://…/p.jpg", "caption"))
client.Send(ctx, to, whatsapp.DocumentByLink("https://…/d.pdf", "invoice.pdf", ""))
client.Send(ctx, to, whatsapp.Location{Latitude: 12.97, Longitude: 77.59, Name: "HQ"})
client.Send(ctx, to, whatsapp.Reaction{MessageID: "wamid.X", Emoji: "👍"})

// Reply to a received message, quoting it in the chat:
client.SendReply(ctx, to, m.ID, whatsapp.Text{Body: "got it 👍"}) // m is an InboundMessage

// Interactive reply buttons (up to 3):
client.Send(ctx, to, whatsapp.InteractiveButtons{
	Body: "Confirm your order?",
	Buttons: []whatsapp.Button{
		{ID: "confirm", Title: "Confirm"},
		{ID: "cancel", Title: "Cancel"},
	},
})

// Interactive list menu:
client.Send(ctx, to, whatsapp.InteractiveList{
	Body: "Choose a plan", ButtonText: "View plans",
	Sections: []whatsapp.ListSection{{
		Title: "Plans",
		Rows: []whatsapp.ListRow{
			{ID: "basic", Title: "Basic", Description: "$5/mo"},
			{ID: "pro", Title: "Pro", Description: "$20/mo"},
		},
	}},
})

// Template with body parameters:
client.SendTemplate(ctx, to, whatsapp.TemplateMessage{
	Name:       "match_mutual",
	Language:   "en_US",
	Components:  []whatsapp.TemplateComponent{whatsapp.BodyComponent("Asha", "Bengaluru")},
})

Error handling

_, err := client.SendTemplate(ctx, to, tmpl)
switch {
case errors.Is(err, whatsapp.ErrReengagementRequired): // 24h window closed
case errors.Is(err, whatsapp.ErrRateLimited):          // back off; Retry-After honored
case errors.Is(err, whatsapp.ErrTemplateNotApproved):  // fix/await template
}

var apiErr *whatsapp.APIError
if errors.As(err, &apiErr) {
	log.Printf("code=%d trace=%s", apiErr.Code, apiErr.FBTraceID)
}

Webhooks

http.Handle("/webhook/whatsapp", whatsapp.NewWebhookHandler(
	whatsapp.WebhookConfig{AppSecret: appSecret, VerifyToken: verifyToken},
	whatsapp.Handlers{
		OnStatus: func(ctx context.Context, s whatsapp.MessageStatus) {
			log.Printf("%s -> %s", s.MessageID, s.Status)
		},
		OnMessage: func(ctx context.Context, m whatsapp.InboundMessage) {
			log.Printf("from %s: %s", m.From, m.Text)
		},
	},
))

The handler answers the GET subscription challenge, verifies the X-Hub-Signature-256 HMAC on POSTs, and always replies 200 to well-formed payloads (hand off durably in your callbacks and return quickly).

Acknowledge an inbound message (the blue ticks), or show a typing indicator while you prepare a reply (which also marks it read):

client.MarkRead(ctx, m.ID)    // m is the InboundMessage from OnMessage
client.SendTyping(ctx, m.ID)  // shows "typing…" for ~25s

Inbound messages are parsed into typed sub-fields by Type — tap/selection replies, media, location, reactions, and the replied-to message ID:

OnMessage: func(ctx context.Context, m whatsapp.InboundMessage) {
	switch {
	case m.Interactive != nil: // button/list reply
		log.Printf("chose %s (%s)", m.Interactive.ID, m.Interactive.Title)
	case m.Media != nil:       // image/video/audio/document/sticker
		data, mime, _ := client.Download(ctx, m.Media.ID)
		_ = data; _ = mime
	case m.Location != nil:
		log.Printf("at %f,%f", m.Location.Latitude, m.Location.Longitude)
	default:
		log.Printf("text: %s", m.Text)
	}
}

Media upload & download

When you can't host a public URL, upload bytes to get a reusable media ID, and download media users send you:

id, _ := client.Upload(ctx, "invoice.pdf", "application/pdf", file) // file is an io.Reader
client.Send(ctx, to, whatsapp.DocumentByID(id, "invoice.pdf", ""))

data, mime, _ := client.Download(ctx, m.Media.ID) // m.Media from an inbound message

Testing your code

Inject MockTransport to assert what would be sent — no network:

mt := whatsapp.NewMockTransport()
client, _ := whatsapp.New(cfg, whatsapp.WithHTTPClient(mt))

_, _ = client.SendText(ctx, "15551234567", "hi")

req, _ := mt.LastRequest()
// req.Body["type"] == "text"

// Simulate an API error:
mt.WithResponse(429, `{"error":{"code":131056,"message":"rate limited"}}`)

Configuration

Option Purpose
WithHTTPClient(Doer) inject *http.Client / custom round-tripper / mock
WithAPIVersion(string) override DefaultAPIVersion (e.g. "v22.0")
WithBaseURL(string) override the Graph host (proxy/sandbox/test)
WithRetry(max, base) retries for 429/5xx (default 2, 500ms, exp backoff)
WithLogger(*slog.Logger) structured logging

Status & roadmap

Phase 1 (send + webhooks) and Phase 2 (interactive messages, rich inbound parsing, media upload/download, typing indicator) are implemented. Template management and Flows are planned — see DESIGN.md. API may change during v0.x; see CHANGELOG.md.

License

MIT © KARTIKrocks

Documentation

Overview

Package whatsapp is a Go client for the WhatsApp Cloud API (Meta Graph API).

It sends messages — text, media, location, reactions, and approved templates (including authentication templates for OTP delivery) — and parses inbound webhooks (delivery statuses and user messages) with signature verification. The library targets Meta's Cloud API directly; it is not a BSP wrapper and has zero third-party dependencies.

Design

WhatsApp Cloud is a single vendor, so rather than the provider-per-module shape of a multi-vendor library, gowhatsapp keeps one module and makes the future-proofing axes explicit:

  • The HTTP boundary is a swappable Doer seam — inject any *http.Client, a custom round-tripper, or MockTransport in tests.
  • The Graph API version is pinned in DefaultAPIVersion and overridable per client, isolating callers from Meta's quarterly version churn.
  • Message is a sealed interface, so new message types are additive and never break existing callers.

Quick start

client, err := whatsapp.New(whatsapp.Config{
    PhoneNumberID: "1234567890",
    AccessToken:   "EAAG...",
})
if err != nil {
    log.Fatal(err)
}

// Inside an open 24-hour service window: free-form text.
_, err = client.SendText(ctx, "15551234567", "Hello from gowhatsapp!")

// To start a conversation or send outside the window: an approved template.
_, err = client.SendTemplate(ctx, "15551234567",
    whatsapp.AuthTemplate("otp_login", "en_US", "472913"))

OTP delivery

AuthTemplate formats an authentication-category template carrying a code. Meta only delivers the code — it does not generate or verify it. Keep code generation and verification in your application.

Webhooks

Mount NewWebhookHandler to receive delivery statuses and inbound messages; it handles the subscription challenge and verifies X-Hub-Signature-256.

http.Handle("/webhook/whatsapp", whatsapp.NewWebhookHandler(
    whatsapp.WebhookConfig{AppSecret: appSecret, VerifyToken: verifyToken},
    whatsapp.Handlers{
        OnStatus:  func(ctx context.Context, s whatsapp.MessageStatus) { /* ... */ },
        OnMessage: func(ctx context.Context, m whatsapp.InboundMessage) { /* ... */ },
    },
))

Index

Constants

View Source
const (
	// DefaultBaseURL is the Meta Graph API host.
	DefaultBaseURL = "https://graph.facebook.com"
	// DefaultAPIVersion is the Graph API version requests target by default.
	DefaultAPIVersion = "v23.0"
)

Default endpoint settings. The API version is pinned and configurable — the single most important future-proofing lever, since Meta ships a new Graph API version roughly quarterly and deprecates old ones on a ~2-year clock. Override per client with WithAPIVersion or Config.APIVersion.

Variables

View Source
var (
	// ErrInvalidConfig means the client or webhook was configured incorrectly
	// (missing phone number ID, access token, app secret, etc.).
	ErrInvalidConfig = errors.New("whatsapp: invalid configuration")

	// ErrInvalidRecipient means the destination number was empty or malformed.
	ErrInvalidRecipient = errors.New("whatsapp: invalid recipient")

	// ErrInvalidMessage means the message payload was nil or incomplete.
	ErrInvalidMessage = errors.New("whatsapp: invalid message")

	// ErrSendFailed is the generic fallback for an API failure that does not
	// map to a more specific sentinel below.
	ErrSendFailed = errors.New("whatsapp: send failed")

	// ErrUnauthorized means the access token is missing, invalid, or expired
	// (Cloud API codes 0, 190).
	ErrUnauthorized = errors.New("whatsapp: unauthorized")

	// ErrRateLimited means the account or number hit a throughput / messaging
	// limit (codes 4, 80007, 130429, 131048, 131056). Honor Retry-After.
	ErrRateLimited = errors.New("whatsapp: rate limited")

	// ErrReengagementRequired means the 24-hour customer service window is
	// closed, so only an approved template may be sent (code 131047).
	ErrReengagementRequired = errors.New("whatsapp: re-engagement required (24h session window closed)")

	// ErrRecipientNotAllowed means the recipient is not in the allowed list of
	// an app still in development mode (code 131030).
	ErrRecipientNotAllowed = errors.New("whatsapp: recipient not in allowed list")

	// ErrTemplateNotApproved means the named template is missing, paused, or
	// not yet approved, or its parameters do not match (codes 132xxx).
	ErrTemplateNotApproved = errors.New("whatsapp: template not found or not approved")

	// ErrMediaError means a media asset could not be fetched or was rejected
	// (codes 131052, 131053).
	ErrMediaError = errors.New("whatsapp: media error")

	// ErrTransient means a temporary upstream failure that is safe to retry
	// (codes 131000, 131016, 131026, and 5xx responses).
	ErrTransient = errors.New("whatsapp: transient server error")

	// ErrInvalidSignature means a webhook payload failed X-Hub-Signature-256
	// verification.
	ErrInvalidSignature = errors.New("whatsapp: invalid webhook signature")

	// ErrUnsupported marks a capability not yet implemented by this library.
	ErrUnsupported = errors.New("whatsapp: operation not supported")
)

Sentinel errors. Every error returned by the library wraps one of these, so callers can branch with errors.Is without parsing message strings.

The Cloud API surfaces failures as numeric codes; APIError carries the full detail while its Unwrap target is one of these sentinels (see the code → sentinel mapping in classify).

Functions

func NewWebhookHandler

func NewWebhookHandler(cfg WebhookConfig, h Handlers) http.Handler

NewWebhookHandler returns an http.Handler implementing the full Cloud API webhook contract:

  • GET verifies the subscription challenge (hub.mode/hub.verify_token → echoes hub.challenge).
  • POST verifies the signature (when AppSecret is set), parses the payload, and dispatches each status/message to the matching callback.

It always replies 200 to a signature-valid POST — even when the body cannot be parsed — so Meta does not redeliver a payload that would never parse anyway; the only rejections are a failed GET handshake (403) and a bad signature (401). Process callbacks should hand off durably (queue/store) and return quickly.

func VerifySignature

func VerifySignature(appSecret string, body []byte, header string) error

VerifySignature checks an X-Hub-Signature-256 header against the raw body using HMAC-SHA256 keyed by appSecret. It is constant-time.

Types

type APIError

type APIError struct {
	// HTTPStatus is the HTTP status code of the response.
	HTTPStatus int
	// Code is the Cloud API error code.
	Code int
	// Subcode is the error_subcode, when present.
	Subcode int
	// Type is the error type string (e.g. "OAuthException").
	Type string
	// Title is a short human title, when the API provides one.
	Title string
	// Message is the human-readable error message.
	Message string
	// Details is the error_data.details field, when present.
	Details string
	// FBTraceID is Meta's trace identifier — quote it in support tickets.
	FBTraceID string
	// Raw is the unparsed response body.
	Raw []byte
	// contains filtered or unexported fields
}

APIError is the structured form of a WhatsApp Cloud API error response. It implements error and unwraps to one of the package sentinels so that both errors.Is(err, whatsapp.ErrRateLimited) and a *APIError type assertion work against the same value.

Reference: https://developers.facebook.com/docs/whatsapp/cloud-api/support/error-codes

func (*APIError) Error

func (e *APIError) Error() string

Error implements the error interface.

func (*APIError) Retryable

func (e *APIError) Retryable() bool

Retryable reports whether retrying the request might succeed.

func (*APIError) Unwrap

func (e *APIError) Unwrap() error

Unwrap returns the mapped sentinel so errors.Is works.

type Button

type Button struct {
	ID    string
	Title string
}

Button is one quick-reply button in an InteractiveButtons message. ID is echoed back in the webhook when tapped; Title is the visible label.

type CapturedRequest

type CapturedRequest struct {
	Method  string
	URL     string
	Header  http.Header
	RawBody []byte
	// Body is RawBody decoded as JSON, for convenient assertions.
	Body map[string]any
}

CapturedRequest is one request the mock observed.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is a WhatsApp Cloud API client for a single phone number. It is safe for concurrent use.

func New

func New(cfg Config, opts ...Option) (*Client, error)

New constructs a Client. PhoneNumberID and AccessToken are required.

func (*Client) DeleteMedia

func (c *Client) DeleteMedia(ctx context.Context, mediaID string) error

DeleteMedia deletes an uploaded media asset by ID.

func (*Client) Download

func (c *Client) Download(ctx context.Context, mediaID string) ([]byte, string, error)

Download fetches the bytes of a media asset by ID, returning the content and its MIME type. It resolves the asset's short-lived URL with Client.MediaInfo and then downloads it with the required authentication.

func (*Client) MarkRead

func (c *Client) MarkRead(ctx context.Context, messageID string) error

MarkRead marks a previously received message as read (the blue ticks shown to the sender). Pass the inbound message's ID (the wamid... from InboundMessage.ID). The Cloud API returns no message ID for this call, so only an error is reported.

func (*Client) MediaInfo

func (c *Client) MediaInfo(ctx context.Context, mediaID string) (*MediaInfo, error)

MediaInfo fetches metadata (including the short-lived download URL) for a media ID, typically one received on an InboundMedia.

func (*Client) Send

func (c *Client) Send(ctx context.Context, to string, msg Message) (*Result, error)

Send delivers any Message to a recipient in E.164 form. A leading "+" and surrounding whitespace are accepted and stripped (the Cloud API wants the bare digits). The returned Result confirms acceptance and carries the message ID for correlating later webhook status events.

func (*Client) SendReply

func (c *Client) SendReply(ctx context.Context, to, replyToID string, msg Message) (*Result, error)

SendReply sends a message as a reply to a previously received message, quoting it in the recipient's chat. replyToID is the message being replied to (the wamid... from InboundMessage.ID or a prior Result.MessageID). Any Message type may be sent as a reply.

func (*Client) SendTemplate

func (c *Client) SendTemplate(ctx context.Context, to string, tmpl TemplateMessage) (*Result, error)

SendTemplate sends an approved message template.

func (*Client) SendText

func (c *Client) SendText(ctx context.Context, to, body string) (*Result, error)

SendText sends a plain text message (valid only inside an open 24-hour service window).

func (*Client) SendTyping

func (c *Client) SendTyping(ctx context.Context, messageID string) error

SendTyping displays a typing indicator to the user in reply to their message (messageID, the wamid... from InboundMessage.ID). WhatsApp dismisses it when you send your next message or after ~25 seconds, whichever comes first. This also marks the message as read, so there is no need to also call Client.MarkRead.

func (*Client) Upload

func (c *Client) Upload(ctx context.Context, filename, mimeType string, r io.Reader) (string, error)

Upload stores a media file with WhatsApp and returns its media ID, which can then be sent with ImageByID (and the other *ByID constructors). mimeType must be a content type WhatsApp accepts for the asset (e.g. "image/jpeg"). The reader is consumed fully and buffered in memory.

type Config

type Config struct {
	// PhoneNumberID is the Cloud API phone number ID messages are sent from.
	PhoneNumberID string
	// AccessToken is the system-user (preferably permanent) access token.
	AccessToken string
	// BusinessAccountID (WABA ID) — reserved for the template-management API.
	BusinessAccountID string
	// APIVersion overrides DefaultAPIVersion (e.g. "v22.0").
	APIVersion string
	// AppSecret signs/verifies webhook payloads (X-Hub-Signature-256).
	AppSecret string
	// WebhookVerifyToken is echoed during webhook subscription verification.
	WebhookVerifyToken string
	// BaseURL overrides DefaultBaseURL (proxy, sandbox, or test server).
	BaseURL string
}

Config holds the credentials and endpoint settings for a Client. Only PhoneNumberID and AccessToken are required to send; AppSecret and WebhookVerifyToken are needed for the webhook handler; BusinessAccountID is reserved for template management (a future addition).

type Doer

type Doer interface {
	Do(req *http.Request) (*http.Response, error)
}

Doer is the HTTP seam: anything that can execute a request. *http.Client satisfies it, as does MockTransport for tests and any custom round-tripper (proxy, instrumentation, on-prem gateway). This is the single point where network behavior is injected, keeping the client itself transport-agnostic.

type Handlers

type Handlers struct {
	// OnStatus fires for each delivery status update (sent/delivered/read/failed).
	OnStatus func(ctx context.Context, s MessageStatus)
	// OnMessage fires for each inbound message from a user.
	OnMessage func(ctx context.Context, m InboundMessage)
}

Handlers carries the per-event callbacks invoked by the webhook handler. Any nil callback is skipped, so you only wire up what you need.

type InboundLocation

type InboundLocation struct {
	Latitude  float64
	Longitude float64
	Name      string
	Address   string
}

InboundLocation is a received location pin.

type InboundMedia

type InboundMedia struct {
	ID       string
	MimeType string
	SHA256   string
	Caption  string
	Filename string // documents only
	Voice    bool   // audio recorded as a voice note
}

InboundMedia identifies a received media asset. Use Client.Download with ID to fetch the bytes.

type InboundMessage

type InboundMessage struct {
	// From is the sender's wa_id.
	From string
	// ID is the wamid... of the inbound message.
	ID string
	// Timestamp is the Unix timestamp string from the event.
	Timestamp string
	// Type is the message type ("text", "image", "interactive", "location", ...).
	Type string
	// Text is the body for text messages.
	Text string
	// ButtonText is the label for legacy quick-reply (template) button replies.
	ButtonText string
	// ButtonPayload is the payload for legacy quick-reply (template) replies.
	ButtonPayload string
	// ContextID is the wamid... of the message this one replies to, when the
	// user replied to (quoted) a previous message.
	ContextID string
	// Interactive is set when the user tapped a reply button or picked a list
	// row from an InteractiveButtons / InteractiveList message.
	Interactive *InteractiveReply
	// Media is set for image, video, audio, document, and sticker messages.
	Media *InboundMedia
	// Location is set for location messages.
	Location *InboundLocation
	// Reaction is set for reaction messages.
	Reaction *InboundReaction
	// Raw is the unparsed message object for types this struct does not model.
	Raw json.RawMessage
}

InboundMessage is a message received from a user. Beyond the always-present envelope fields (From, ID, Timestamp, Type), the typed sub-fields are populated according to Type: Text for "text"; Interactive for "interactive" (button/list replies); Media for "image"/"video"/"audio"/"document"/ "sticker"; Location for "location"; Reaction for "reaction". Unmodeled types are still available via Raw.

type InboundReaction

type InboundReaction struct {
	MessageID string
	Emoji     string
}

InboundReaction is a received reaction to one of your messages. Emoji is empty when the user removed their reaction.

type InteractiveButtons

type InteractiveButtons struct {
	Body    string
	Header  string
	Footer  string
	Buttons []Button
}

InteractiveButtons is a message with up to three quick-reply buttons. Body is required; Header (text) and Footer are optional.

type InteractiveList

type InteractiveList struct {
	Body       string
	Header     string
	Footer     string
	ButtonText string
	Sections   []ListSection
}

InteractiveList is a menu the user opens via a single button to pick one row from one or more sections. Body and ButtonText are required; Header (text) and Footer are optional.

type InteractiveReply

type InteractiveReply struct {
	Kind  string
	ID    string
	Title string
}

InteractiveReply is a user's selection from an interactive message. Kind is "button_reply" or "list_reply"; ID is the developer-assigned identifier set when sending; Title is the visible label the user saw.

type ListRow

type ListRow struct {
	ID          string
	Title       string
	Description string
}

ListRow is one selectable row inside a ListSection. ID is echoed back in the webhook when chosen; Description is optional secondary text.

type ListSection

type ListSection struct {
	Title string
	Rows  []ListRow
}

ListSection groups rows under an optional heading in an InteractiveList.

type Location

type Location struct {
	Latitude  float64
	Longitude float64
	Name      string
	Address   string
}

Location is a location pin message.

type Media

type Media struct {
	ID       string
	Link     string
	Caption  string
	Filename string
	// contains filtered or unexported fields
}

Media is an image, document, audio, video, or sticker message. Reference the asset either by a public Link or by an uploaded media ID (the Media upload API is a future addition); set exactly one. Caption applies to image, video, and document; Filename applies to document.

func AudioByID

func AudioByID(id string) Media

AudioByID sends a previously uploaded audio clip by media ID.

func AudioByLink(link string) Media

AudioByLink sends an audio clip referenced by a public URL.

func DocumentByID

func DocumentByID(id, filename, caption string) Media

DocumentByID sends a previously uploaded document by media ID.

func DocumentByLink(link, filename, caption string) Media

DocumentByLink sends a document referenced by a public URL.

func ImageByID

func ImageByID(id, caption string) Media

ImageByID sends a previously uploaded image by media ID.

func ImageByLink(link, caption string) Media

ImageByLink sends an image referenced by a public URL.

func StickerByID

func StickerByID(id string) Media

StickerByID sends a previously uploaded sticker by media ID.

func StickerByLink(link string) Media

StickerByLink sends a sticker referenced by a public URL.

func VideoByID

func VideoByID(id, caption string) Media

VideoByID sends a previously uploaded video by media ID.

func VideoByLink(link, caption string) Media

VideoByLink sends a video referenced by a public URL.

type MediaInfo

type MediaInfo struct {
	ID       string
	URL      string
	MimeType string
	SHA256   string
	FileSize int64
}

MediaInfo is the metadata WhatsApp stores for an uploaded or received media asset. URL is a short-lived, authenticated download link (fetch it via Client.Download, which adds the required bearer token).

type Message

type Message interface {
	// contains filtered or unexported methods
}

Message is a sendable WhatsApp message. The interface is sealed: only this package can implement it (via the unexported buildRequest method). That is a deliberate API-compatibility choice — new message types can be added in future releases without breaking callers, because no external code can hold an alternative implementation that the client wouldn't understand.

Construct values directly (Text{...}) or via the typed constructors (TextMessage, ImageByLink, AuthTemplate, ...).

type MessageStatus

type MessageStatus struct {
	// MessageID is the wamid... of the message this status refers to.
	MessageID string
	// Status is "sent", "delivered", "read", or "failed".
	Status string
	// Timestamp is the Unix timestamp string from the event.
	Timestamp string
	// RecipientID is the recipient's wa_id.
	RecipientID string
	// ConversationID identifies the billing conversation, when present.
	ConversationID string
	// Category is the conversation origin category (e.g. "marketing",
	// "utility", "authentication", "service").
	Category string
	// Errors carries failure detail when Status == "failed".
	Errors []APIError
}

MessageStatus is an outbound message delivery status update.

type MockTransport

type MockTransport struct {
	// contains filtered or unexported fields
}

MockTransport is a Doer for tests. It records every request (with the decoded WhatsApp payload) and returns a programmable response, so consumer code can assert on what would have been sent without touching the network.

mt := whatsapp.NewMockTransport()
c, _ := whatsapp.New(cfg, whatsapp.WithHTTPClient(mt))
_, _ = c.SendText(ctx, "15551234567", "hi")
req, _ := mt.LastRequest()
// assert req.Body["type"] == "text"

It is safe for concurrent use.

func NewMockTransport

func NewMockTransport() *MockTransport

NewMockTransport returns a mock that responds 200 with a stub message ID.

func (*MockTransport) Do

func (m *MockTransport) Do(req *http.Request) (*http.Response, error)

Do implements Doer.

func (*MockTransport) LastRequest

func (m *MockTransport) LastRequest() (CapturedRequest, bool)

LastRequest returns the most recent captured request, if any.

func (*MockTransport) Requests

func (m *MockTransport) Requests() []CapturedRequest

Requests returns a copy of all captured requests, oldest first.

func (*MockTransport) Reset

func (m *MockTransport) Reset()

Reset clears captured requests and programmed responses.

func (*MockTransport) WithError

func (m *MockTransport) WithError(err error) *MockTransport

WithError programs a transport-level (network) error for every request.

func (*MockTransport) WithResponse

func (m *MockTransport) WithResponse(status int, body string) *MockTransport

WithResponse programs the HTTP status and body returned for every request. Use it to simulate API errors:

mt.WithResponse(429, `{"error":{"code":131056,"message":"rate limited"}}`)

type Option

type Option func(*Client)

Option customizes a Client at construction.

func WithAPIVersion

func WithAPIVersion(version string) Option

WithAPIVersion overrides the Graph API version for this client.

func WithBaseURL

func WithBaseURL(baseURL string) Option

WithBaseURL overrides the API host (trailing slash trimmed).

func WithHTTPClient

func WithHTTPClient(d Doer) Option

WithHTTPClient injects the HTTP doer used for all requests. Pass an *http.Client with your preferred timeout, transport, or instrumentation, or a MockTransport in tests.

func WithLogger

func WithLogger(l *slog.Logger) Option

WithLogger sets the structured logger.

func WithRetry

func WithRetry(maxRetries int, base time.Duration) Option

WithRetry configures retry behavior for transient failures (429 / 5xx). maxRetries is the number of additional attempts; base is the initial backoff (doubled each attempt, unless the response carries Retry-After).

type Reaction

type Reaction struct {
	MessageID string
	Emoji     string
}

Reaction reacts to a previously received message with an emoji. Send an empty Emoji to remove a reaction.

type Result

type Result struct {
	// MessageID is the wamid... identifier. Match it against the message_id of
	// later webhook status events (sent/delivered/read/failed).
	MessageID string
	// WAID is the recipient's resolved WhatsApp ID, when returned.
	WAID string
	// Input is the recipient address as the API echoed it back.
	Input string
	// Raw is the unparsed success response body.
	Raw []byte
}

Result is the outcome of a successful send. The Cloud API accepts a message for delivery asynchronously, so a Result confirms acceptance (and yields the message ID to correlate later webhook status updates) — not final delivery.

type TemplateComponent

type TemplateComponent struct {
	// Type is "header", "body", or "button".
	Type string
	// SubType applies to buttons: "url", "quick_reply", "copy_code", "flow".
	SubType string
	// Index is the zero-based button position; required for button components.
	Index *int
	// Parameters fill the {{n}} placeholders within the component.
	Parameters []TemplateParameter
}

TemplateComponent is one header, body, or button section of a template.

func BodyComponent

func BodyComponent(params ...string) TemplateComponent

BodyComponent builds a "body" component from positional text parameters.

type TemplateMessage

type TemplateMessage struct {
	// Name is the approved template name.
	Name string
	// Language is the BCP-47 / Meta locale code (e.g. "en", "en_US"). Defaults
	// to "en_US" when empty.
	Language string
	// Components fill the template's header/body/button placeholders, in order.
	Components []TemplateComponent
}

TemplateMessage is a pre-approved message template send. Templates are the only way to start a conversation or message a user outside the open 24-hour service window. Create and submit templates for approval in WhatsApp Manager (template management via API is a future addition); reference them here by Name plus Language.

func AuthTemplate

func AuthTemplate(name, language, code string) TemplateMessage

AuthTemplate builds an authentication-category template carrying a one-time code, the standard path for OTP delivery over WhatsApp. It fills the body placeholder with the code and the copy-code / one-tap URL button parameter (button index 0) that Meta's authentication templates require.

Important: Meta does NOT generate or verify the code — it only delivers it. Generation and verification stay in your application. This helper just formats the delivery payload.

type TemplateParameter

type TemplateParameter struct {
	// Type is "text", "payload", "coupon_code", "currency", "date_time",
	// "image", "document", or "video".
	Type string
	// Text carries the value for text/payload/coupon_code parameters.
	Text string
}

TemplateParameter is a single substitution value for a template placeholder. For the common text case use TextParam; richer parameter kinds (currency, date_time, media) are supported by setting Type accordingly.

func TextParam

func TextParam(text string) TemplateParameter

TextParam builds a text template parameter.

type Text

type Text struct {
	Body string
	// PreviewURL enables link previews when Body contains a URL.
	PreviewURL bool
}

Text is a plain text message. Free-form text may only be sent inside an open 24-hour customer service window; outside it, use a Template.

func TextMessage

func TextMessage(body string) Text

TextMessage is a convenience constructor for a Text message.

type WebhookConfig

type WebhookConfig struct {
	// AppSecret verifies the X-Hub-Signature-256 payload signature. When empty,
	// signature verification is skipped (not recommended outside tests).
	AppSecret string
	// VerifyToken is matched during the GET subscription handshake.
	VerifyToken string
	// Logger records unparseable payloads (which are answered with 200 to avoid
	// pointless Meta redelivery). Defaults to slog.Default() when nil.
	Logger *slog.Logger
}

WebhookConfig configures webhook verification.

type WebhookEvent

type WebhookEvent struct {
	Object   string
	Metadata WebhookMetadata
	Statuses []MessageStatus
	Messages []InboundMessage
}

WebhookEvent is a parsed webhook notification, flattened across the nested entry/changes envelope into the two lists callers actually care about.

func ParseEvent

func ParseEvent(body []byte) (*WebhookEvent, error)

ParseEvent flattens a raw webhook body into a WebhookEvent.

type WebhookMetadata

type WebhookMetadata struct {
	DisplayPhoneNumber string
	PhoneNumberID      string
}

WebhookMetadata identifies the receiving number.

Jump to

Keyboard shortcuts

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