wapi.go is a Go SDK for the WhatsApp Business Platform: the Cloud API (sending messages and receiving webhooks) and the Business Management API (business accounts, phone numbers, templates and catalogs).
๐ Full documentation: golang.wapikit.com ยท API reference: pkg.go.dev
This SDK is not affiliated with or endorsed by Meta. It is an open source project to make building on the WhatsApp Cloud API easier. A Node.js version is available as wapi.js.
- Features
- Installation
- Quick start
- Sending messages
- Receiving webhooks
- Business management
- Error handling
- Configuration
- Packages
- Examples
- Contributing
- Messages: send text, media (image, video, audio, document, sticker), location, contacts, reactions, templates and interactive messages (reply buttons, lists, CTA URL buttons, product, product list and catalog messages, location and contact requests).
- Recipients: send to a phone number or to a business-scoped user id (BSUID).
- Read receipts: mark messages as read, optionally with a typing indicator.
- Media: upload media, resolve a media id to its download URL, and delete media.
- Webhooks: verify the subscription, optionally check
X-Hub-Signature-256, and get typed events for incoming messages, delivery statuses (including pricing) and account notifications. - Integration: run the built-in webhook server, or mount the handlers in your own Echo app.
- Business Management API: business account details and analytics, phone numbers (registration, verification, QR codes, usernames), message templates and product catalogs, with pagination helpers.
- Errors: Graph API failures come back as a typed
GraphAPIErrorwith the HTTP status, Meta's error code and the raw body. - Timeouts: requests share one HTTP client with sensible timeouts. The client, API version and base URL can all be replaced.
Requires Go 1.21 or later.
go get github.com/wapikit/wapi.goYou need a Meta app with the WhatsApp product, a business account id, a phone number id and an access token. The setup guide walks through getting them.
This bot replies "Hello, from wapi.go" to every text message it receives:
package main
import (
"log"
wapi "github.com/wapikit/wapi.go/pkg/client"
"github.com/wapikit/wapi.go/pkg/components"
"github.com/wapikit/wapi.go/pkg/events"
)
func main() {
client := wapi.New(&wapi.ClientConfig{
BusinessAccountId: "<WHATSAPP_BUSINESS_ACCOUNT_ID>",
ApiAccessToken: "<ACCESS_TOKEN>",
WebhookSecret: "<WEBHOOK_VERIFY_TOKEN>", // the verify token you set in the Meta app dashboard
WebhookPath: "/webhook",
WebhookServerPort: 8080,
})
client.On(events.TextMessageEventType, func(event events.BaseEvent) {
message := event.(*events.TextMessageEvent)
reply, err := components.NewTextMessage(components.TextMessageConfigs{
Text: "Hello, from wapi.go",
})
if err != nil {
log.Println("build reply:", err)
return
}
if _, err := message.Reply(reply); err != nil {
log.Println("send reply:", err)
}
})
// Starts the webhook server on 127.0.0.1:8080 and blocks until Ctrl+C.
client.Initiate()
}The built-in server listens on 127.0.0.1 only, so put a reverse proxy or a tunnel such as ngrok in front of it. Then point your Meta app's webhook at https://<your-domain>/webhook and subscribe to the messages field.
Messages are sent through a messaging client for one of your business phone numbers. Every message type has a constructor in pkg/components that validates its input.
messaging := client.NewMessagingClient("<PHONE_NUMBER_ID>")
text, err := components.NewTextMessage(components.TextMessageConfigs{
Text: "Your order has shipped: https://example.com/track/123",
AllowPreview: true,
})
if err != nil {
return err
}
resp, err := messaging.Message.Send(text, "15551234567")
if err != nil {
return err
}
log.Println("sent message", resp.Messages[0].ID)image, err := components.NewImageMessage(components.ImageMessageConfigs{
Link: "https://example.com/cat.png", // or Id: a media id returned by UploadMedia
Caption: "Look at this",
})
if err != nil {
return err
}
_, err = messaging.Message.Send(image, "15551234567")Upload a file once and send it by id:
file, err := os.Open("invoice.pdf")
if err != nil {
return err
}
defer file.Close()
mediaId, err := messaging.Media.UploadMedia("<PHONE_NUMBER_ID>", file, "invoice.pdf", "application/pdf")Media.GetMediaUrlById(id) resolves the media id from an incoming message into a download URL. Media.DeleteMedia(id) deletes uploaded media.
Outside the 24-hour customer service window, only approved templates can be sent:
template, err := components.NewTemplateMessage(&components.TemplateMessageConfigs{
Name: "order_shipped",
Language: "en_US",
})
if err != nil {
return err
}
name := "Ada"
template.AddBody(components.TemplateMessageComponentBodyType{
Type: components.TemplateMessageComponentTypeBody,
Parameters: []components.TemplateMessageParameter{
components.TemplateMessageBodyAndHeaderParameter{
Type: components.TemplateMessageParameterTypeText,
Text: &name,
},
},
})
_, err = messaging.Message.Send(template, "15551234567")AddHeader, AddButton and AddCarousel add the other template components. Message.SendMarketingMessage sends through the Marketing Messages API, for business accounts onboarded to it.
buttons, err := components.NewQuickReplyButtonMessage("How was your delivery?")
if err != nil {
return err
}
if err := buttons.AddButton("good", "Great"); err != nil {
return err
}
if err := buttons.AddButton("bad", "Not great"); err != nil {
return err
}
_, err = messaging.Message.Send(buttons, "15551234567")Lists (NewListMessage), CTA URL buttons (NewCtaMessage), products (NewProductMessage, NewProductListMessage, NewCatalogMessage) and location and contact requests (NewLocationRequestMessage, NewContactRequestMessage) follow the same pattern.
// Reply in-thread to a message you received:
messaging.Message.Reply(text, "15551234567", "<INCOMING_MESSAGE_ID>")
// Or, inside an event handler:
message.Reply(text)
message.React("๐")
// Mark a message as read, with or without a typing indicator:
messaging.Message.ReadMessageOnly("<INCOMING_MESSAGE_ID>")
messaging.Message.ReadMessageWithTyping("<INCOMING_MESSAGE_ID>")Users who chat through a WhatsApp username may only be known by their BSUID. Send to them with a message target:
import "github.com/wapikit/wapi.go/manager"
resp, err := messaging.Message.SendToTarget(text, manager.NewBSUIDTarget("<BSUID>"))manager.NewPhoneTarget and manager.NewParentBSUIDTarget build the other target types. ReplyToTarget and SendMarketingMessageToTarget are the target-based versions of Reply and SendMarketingMessage.
Register a handler per event type with client.On. Each handler runs in its own goroutine and receives every event of its type, one at a time. Type-assert the event to its concrete pointer type:
client.On(events.ImageMessageEventType, func(event events.BaseEvent) {
image := event.(*events.ImageMessageEvent)
url, err := messaging.Media.GetMediaUrlById(image.MediaId)
// download url with your access token ...
})
client.On(events.MessageFailedEventType, func(event events.BaseEvent) {
failed := event.(*events.MessageFailedEvent)
log.Printf("message %s to %s failed: %d %s", failed.MessageId, failed.SentTo, failed.ErrorCode, failed.ErrorMessage)
})| Category | Event types |
|---|---|
| Incoming messages | TextMessageEventType, ImageMessageEventType, VideoMessageEventType, AudioMessageEventType, DocumentMessageEventType, StickerMessageEventType, LocationMessageEventType, ContactMessageEventType, ReactionMessageEventType |
| Interactions | QuickReplyMessageEventType, ReplyButtonInteractionEventType, ListInteractionMessageEventType, OrderReceivedEventType, AdInteractionEventType |
| Delivery statuses | MessageSentEventType, MessageDeliveredEventType, MessageReadEventType, MessageFailedEventType, MessageUndeliveredEventType |
| Account and identity | AccountAlertsEventType, AccountUpdateEventType, AccountReviewUpdateEventType, BusinessCapabilityUpdateEventType, SecurityEventType, PhoneNumberNameUpdateEventType, PhoneNumberQualityUpdateEventType, UserPreferencesEventType, UserIdUpdateEventType, BusinessUsernameUpdateEventType, CustomerNumberChangedEventType |
| Templates | MessageTemplateStatusUpdateEventType, MessageTemplateQualityUpdateEventType, TemplateCategoryUpdateEventType, MessageTemplateComponentsUpdateEventType |
| Other | PaymentConfigurationUpdateEventType, HistoryEventType, SmbAppStateSyncEventType, SmbMessageEchoesEventType, ReadyEventType |
Business-management webhooks each arrive under their own event type, and webhook fields the library doesn't support are acknowledged and ignored.
Status events carry Meta's pricing block in Pricing when it is present. Message events carry the sender's identity: From, SenderName, and the BSUID fields UserId, ParentUserId and Username.
If one message or change in a webhook delivery can't be parsed, it is logged with log/slog and skipped, and the rest of the batch is still delivered. A panic in your handler is recovered and logged, so it can't take down the process.
Instead of client.Initiate(), mount the handlers on an existing Echo app:
e := echo.New()
e.GET("/webhook", client.GetWebhookGetRequestHandler()) // subscription verification
e.POST("/webhook", client.GetWebhookPostRequestHandler()) // event delivery
e.Logger.Fatal(e.Start(":8080"))Meta signs every webhook delivery with your app secret. Set WebhookAppSecret and the POST handler rejects any request whose X-Hub-Signature-256 header doesn't match the raw body, returning 401:
client := wapi.New(&wapi.ClientConfig{
// ...
WebhookAppSecret: os.Getenv("META_APP_SECRET"),
})If you need to check the signature yourself (for example in middleware that changes the body), use wapi.VerifyWebhookSignature(appSecret, body, header).
client.Business wraps the Business Management API for the configured business account:
account, err := client.Business.Fetch()
phone, err := client.Business.PhoneNumber.Fetch("<PHONE_NUMBER_ID>")
template, err := client.Business.Template.Fetch("<TEMPLATE_ID>")
// Page through templates:
page, err := client.Business.Template.FetchAllWithPagination(&manager.PaginationInput{Limit: 50})
for err == nil && page.Paging.HasNextPage() {
page, err = client.Business.Template.FetchAllWithPagination(&manager.PaginationInput{
Limit: 50,
After: page.Paging.GetNextCursor(),
})
}
// Or fetch every page at once:
templates, err := client.Business.Template.GetAllTemplates()
catalogs, err := client.Business.Catalog.GetAllCatalogsWithPagination()
products, err := client.Business.Catalog.GetAllCatalogProducts("<CATALOG_ID>")Also available:
- Phone numbers:
Create,RequestVerificationCode,VerifyCode, QR codes (GenerateQrCode,GetAllQrCodes,GetQrCodeById,UpdateQrCode,DeleteQrCode) and usernames (GetUsername,SetUsername,DeleteUsername). - Templates:
Create,Update,Delete(by ID),DeleteByNameandMigrateFromOtherBusinessAccount. - Analytics:
client.Business.FetchAnalytics(sent and delivered counts) andclient.Business.FetchConversationAnalytics(conversation counts and cost).ConversationAnalyticsstill works but is deprecated because it rounds cost to a whole number. - Registration:
messaging.Register(pin)andmessaging.Deregister()register a phone number with the Cloud API and remove it again.
When the Graph API responds with a non-2xx status, the error is a *wapi.GraphAPIError. It carries the HTTP status, Meta's error code, subcode, message and fbtrace_id, and the raw response body:
resp, err := messaging.Message.Send(text, "15551234567")
if apiErr, ok := wapi.AsGraphAPIError(err); ok {
log.Printf("graph error: status=%d code=%d trace=%s: %s",
apiErr.StatusCode, apiErr.Code, apiErr.FBTraceID, apiErr.Message)
if apiErr.IsRetryable() { // 429 or 5xx
// back off and retry
}
}For message sends, the parsed response is returned along with the error when Meta includes one, so resp.Error gives you Meta's error_data.details too. Network failures and timeouts are returned as the standard net/http errors.
Every field below WebhookServerPort is optional:
client := wapi.New(&wapi.ClientConfig{
BusinessAccountId: "...",
ApiAccessToken: "...",
WebhookSecret: "...", // webhook verify token (required)
WebhookPath: "/webhook",
WebhookServerPort: 8080,
WebhookAppSecret: "...", // check X-Hub-Signature-256 on deliveries
HTTPClient: &http.Client{Timeout: 2 * time.Minute}, // replace the default HTTP client
ApiVersion: "v21.0", // Graph API version (default "v20.0")
GraphBaseURL: "http://127.0.0.1:9999", // point at a test server
})- Default HTTP client: shared by all clients. It bounds connecting, the TLS handshake and waiting for Meta's response headers (60s), but sets no overall timeout, so large media uploads are not cut off.
- Bounding calls: manager methods don't take a
contextyet. To cap how long any call can take, pass anHTTPClientwith aTimeout. - Testing:
GraphBaseURLmakes it easy to test your integration against anhttptest.Server. - Logging: the library logs through
log/slog's default logger. Callslog.SetDefaultto route its logs into your own logger.
| Package | Import path | Contents |
|---|---|---|
wapi |
github.com/wapikit/wapi.go/pkg/client |
New, ClientConfig, webhook handlers, GraphAPIError |
components |
github.com/wapikit/wapi.go/pkg/components |
Message builders for every message type |
events |
github.com/wapikit/wapi.go/pkg/events |
Event types and event structs |
manager |
github.com/wapikit/wapi.go/manager |
Message, media, template, phone number and catalog managers, response types, message targets |
business |
github.com/wapikit/wapi.go/pkg/business |
Business account client (reached through client.Business) |
messaging |
github.com/wapikit/wapi.go/pkg/messaging |
Per-phone-number messaging client (from client.NewMessagingClient) |
examples/chat-bot: a bot that answers keywords with each message type, using the built-in server.examples/http-backend-integration: the webhook handlers mounted on your own Echo server.
Contributions are welcome. See CONTRIBUTING.md for the workflow and COMMIT_CONVENTION.md for commit messages; releases are cut automatically from Conventional Commits. Before opening a PR, run:
gofmt -l . && go vet ./... && go test -race ./...Report bugs and ideas on GitHub issues.
Distributed under the Apache 2.0 License. See LICENSE.
- Email: sarthak@softlancer.co
- Twitter | LinkedIn