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
- Variables
- func NewWebhookHandler(cfg WebhookConfig, h Handlers) http.Handler
- func VerifySignature(appSecret string, body []byte, header string) error
- type APIError
- type Button
- type CapturedRequest
- type Client
- func (c *Client) DeleteMedia(ctx context.Context, mediaID string) error
- func (c *Client) Download(ctx context.Context, mediaID string) ([]byte, string, error)
- func (c *Client) MarkRead(ctx context.Context, messageID string) error
- func (c *Client) MediaInfo(ctx context.Context, mediaID string) (*MediaInfo, error)
- func (c *Client) Send(ctx context.Context, to string, msg Message) (*Result, error)
- func (c *Client) SendReply(ctx context.Context, to, replyToID string, msg Message) (*Result, error)
- func (c *Client) SendTemplate(ctx context.Context, to string, tmpl TemplateMessage) (*Result, error)
- func (c *Client) SendText(ctx context.Context, to, body string) (*Result, error)
- func (c *Client) SendTyping(ctx context.Context, messageID string) error
- func (c *Client) Upload(ctx context.Context, filename, mimeType string, r io.Reader) (string, error)
- type Config
- type Doer
- type Handlers
- type InboundLocation
- type InboundMedia
- type InboundMessage
- type InboundReaction
- type InteractiveButtons
- type InteractiveList
- type InteractiveReply
- type ListRow
- type ListSection
- type Location
- type Media
- func AudioByID(id string) Media
- func AudioByLink(link string) Media
- func DocumentByID(id, filename, caption string) Media
- func DocumentByLink(link, filename, caption string) Media
- func ImageByID(id, caption string) Media
- func ImageByLink(link, caption string) Media
- func StickerByID(id string) Media
- func StickerByLink(link string) Media
- func VideoByID(id, caption string) Media
- func VideoByLink(link, caption string) Media
- type MediaInfo
- type Message
- type MessageStatus
- type MockTransport
- func (m *MockTransport) Do(req *http.Request) (*http.Response, error)
- func (m *MockTransport) LastRequest() (CapturedRequest, bool)
- func (m *MockTransport) Requests() []CapturedRequest
- func (m *MockTransport) Reset()
- func (m *MockTransport) WithError(err error) *MockTransport
- func (m *MockTransport) WithResponse(status int, body string) *MockTransport
- type Option
- type Reaction
- type Result
- type TemplateComponent
- type TemplateMessage
- type TemplateParameter
- type Text
- type WebhookConfig
- type WebhookEvent
- type WebhookMetadata
Constants ¶
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 ¶
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") // (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.
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
type Button ¶
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 (*Client) DeleteMedia ¶
DeleteMedia deletes an uploaded media asset by ID.
func (*Client) Download ¶
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 ¶
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 ¶
MediaInfo fetches metadata (including the short-lived download URL) for a media ID, typically one received on an InboundMedia.
func (*Client) Send ¶
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 ¶
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 ¶
SendText sends a plain text message (valid only inside an open 24-hour service window).
func (*Client) SendTyping ¶
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 ¶
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 ¶
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 ¶
InboundReaction is a received reaction to one of your messages. Emoji is empty when the user removed their reaction.
type InteractiveButtons ¶
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
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 ¶
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 ¶
ListRow is one selectable row inside a ListSection. ID is echoed back in the webhook when chosen; Description is optional secondary text.
type ListSection ¶
ListSection groups rows under an optional heading in an InteractiveList.
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 AudioByLink ¶
AudioByLink sends an audio clip referenced by a public URL.
func DocumentByID ¶
DocumentByID sends a previously uploaded document by media ID.
func DocumentByLink ¶
DocumentByLink sends a document referenced by a public URL.
func ImageByLink ¶
ImageByLink sends an image referenced by a public URL.
func StickerByID ¶
StickerByID sends a previously uploaded sticker by media ID.
func StickerByLink ¶
StickerByLink sends a sticker referenced by a public URL.
func VideoByLink ¶
VideoByLink sends a video referenced by a public URL.
type MediaInfo ¶
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) 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 ¶
WithAPIVersion overrides the Graph API version for this client.
func WithBaseURL ¶
WithBaseURL overrides the API host (trailing slash trimmed).
func WithHTTPClient ¶
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.
type Reaction ¶
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 ¶
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 ¶
WebhookMetadata identifies the receiving number.