Go bindings for the WhatsApp Cloud API, targeting Graph API v25.0.
Status: early. The client core (transport, retry, error model) is in place and tested. The message-type surface is currently text-only — see Roadmap.
This module targets the Cloud API (Meta-hosted). The On-Premises API is not supported.
We build with our own tooling:
- SpecScore — specify requirements as
SpecScore.mdartifacts - SpecStudio — author & manage specs across their lifecycle
- inGitDB — store structured data in Git where applicable
- DALgo — data access layer for Go
- cover100.dev — drive toward 100% test coverage
- DataTug — query & explore data
| Package | Import path | Description |
|---|---|---|
wabotapi |
github.com/bots-go-framework/bots-api-whatsapp/wabotapi |
Core Cloud API types and HTTP client |
go get github.com/bots-go-framework/bots-api-whatsapppackage main
import (
"context"
"log"
"github.com/bots-go-framework/bots-api-whatsapp/wabotapi"
)
func main() {
client := wabotapi.NewClient(accessToken, phoneNumberID)
resp, err := client.SendText(context.Background(), "16505551234", "Hello!")
if err != nil {
log.Fatal(err)
}
log.Println("sent:", resp.MessageID())
}WhatsApp only permits free-form messages within 24 hours of the recipient's last
reply. Outside that window, only pre-approved template messages may be sent, and
the API rejects the send with error code 131047.
This has no Telegram analogue, so it is worth handling explicitly:
resp, err := client.SendText(ctx, to, text)
if apiErr := wabotapi.AsAPIError(err); apiErr != nil {
switch {
case apiErr.IsReEngagementRequired():
// The 24h window has closed — send an approved template instead.
case apiErr.IsRateLimited():
// Already retried with backoff; back off further or shed load.
case apiErr.IsUnreachable():
// Recipient cannot be reached; do not retry.
case apiErr.IsAuthError():
// Token missing, invalid, or expired.
}
}Callers that would rather not import this package can test the behavior interfaces directly:
if e, ok := err.(interface{ IsReEngagementRequired() bool }); ok && e.IsReEngagementRequired() {
// ...
}Client.BaseURL is overridable, so the client can be driven against an
httptest.Server:
ts := httptest.NewServer(handler)
defer ts.Close()
c := wabotapi.NewClientWithHTTPClient(token, phoneNumberID, ts.Client())
c.BaseURL = ts.URLClient.GraphVersion defaults to wabotapi.DefaultGraphVersion and can be
overridden per client. Meta deprecates Graph API versions on a rolling ~2 year
schedule; check the
changelog before
relying on the default.
This client follows the conventions of its sibling
bots-api-telegram —
repo skeleton, <Method>Config type naming, embedded-base-then-extend, doc
comments ending in the upstream API URL, and var _ Sendable = (*T)(nil)
assertions.
It deliberately deviates in a few places, each of which is a known wart in the older clients rather than a considered convention:
| Deviation | Why |
|---|---|
BaseURL is an overridable field, not a const |
A const endpoint is why the older clients have no httptest coverage of their core |
ctx is the first parameter of every API call and reaches http.NewRequestWithContext |
Telegram's client holds a context for logging only and never propagates it |
Default http.Client.Timeout |
The older clients use a bare &http.Client{} with no timeout |
| JSON request bodies | url.Values encoding is a Telegram Bot API accommodation; the Cloud API takes JSON |
Retry classifies on error.code, with exponential backoff |
Telegram's client string-matches a GAE error and has no rate-limit handling. See the note below on why not 429/Retry-After. |
| Access token is unexported | An exported, serializable token field is a leak vector |
| Errors never echo request URL or body | The older client formats request params into a 401 error, and those reach logs |
The Cloud API does not signal throttling with HTTP 429, and does not send
a Retry-After header. Meta's error reference mentions neither: rate limits
arrive as HTTP 400 carrying an error code (4, 80007, 130429, 131048,
131064).
This matters because several third-party clients — including at least one Go library — annotate those codes as 429. Building on that assumption yields retry logic that never fires.
So this client classifies on error.code and ignores HTTP status for that
decision. Retry-After is still honored if present, defensively, in case an
edge or proxy throttles ahead of Meta — but nothing depends on it appearing.
For the same reason, never string-match error text: Meta's documented wording for
131047 and the wording that actually arrives on the wire differ, and Meta says
code titles "will eventually be deprecated". Branch on the integer.
Implemented:
- Client core: bearer auth, JSON transport, retry with backoff, typed errors
textmessagestemplatemessages — named and positional body parameters
Not yet implemented:
- Template
headerandbuttoncomponents, and thecurrency/date_time/ media parameter types. Deliberately omitted: Meta's current docs do not spell out their nested shapes, and guessing at a wire format is worse than not shipping it. - Media:
image,audio,document,sticker,video(upload → media ID → send by ID) interactivemessages (buttons, lists)location,contacts,reaction- Inbound webhook payload types,
X-Hub-Signature-256verification, and thehub.challengeverification handshake - Message status callbacks (sent / delivered / read / failed)
// No placeholders.
resp, err := client.SendTemplate(ctx, to, "hello_world", "en_US")
// Positional placeholders: {{1}}, {{2}} ...
resp, err := client.SendTemplate(ctx, to, "order_confirmation", "en_US",
"Jessica", "SKBUP2-4CPIG9")
// Named placeholders: {{first_name}} ...
cfg := wabotapi.NewSendTemplate(to, "order_confirmation", "en_US").
WithNamedBodyParams(
wabotapi.NamedParam{Name: "first_name", Value: "Jessica"},
wabotapi.NamedParam{Name: "order_number", Value: "SKBUP2-4CPIG9"},
)
resp, err := client.SendMessage(ctx, cfg)A template declares its parameter format at creation time and defaults to
positional. A component may not mix the two formats; this client rejects that
locally rather than spending a round trip to earn a 132018 validation error.
- bots-fw — the bot framework this
client is intended to plug into via a
bots-fw-whatsappadapter - bots-api-telegram — the sibling client this one is modelled on