Library / SDK
asternic/wuzapi avatar
asternic/wuzapi

wuzapi: a REST API for WhatsApp built on whatsmeow

Simple RESTful API for WhatsApp in Golang (using the Whatsmeow multi device library)

1,046 stars486 forksGoMIT

At a glance

What is it?
wuzapi is a Go service that exposes WhatsApp sending and management over REST by driving the whatsmeow library, so it talks to WhatsApp's own WebSocket servers instead of a headless browser. It ships multi-device sessions, HMAC signed webhooks and a Compose stack, and the project itself warns that using it against WhatsApp's terms can get a number banned.
Who is it for?
Adopt wuzapi if you need a self-hosted WhatsApp integration for a system you control, want multi-device sessions and webhooks with per-content-type HMAC signing, and can accept that a protocol change on WhatsApp's side will break your connections until whatsmeow is updated.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 3 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

WebSocket to WhatsApp, not a headless browser

WuzAPI is a Go implementation of the @tulir/whatsmeow library wrapped in a RESTful service, with support for multiple devices and concurrent sessions. The design choice that defines the project is the one the documentation makes first: it does not drive Puppeteer on headless Chrome, and it does not run an Android emulator.

Instead, it talks directly to WhatsApp's WebSocket servers. The stated consequence is speed and footprint, since the project describes this as significantly faster and much less demanding on memory and CPU than those approaches. The stated cost is fragility, because any change to the WhatsApp protocol can break connections, and a library update is then required. That trade is the whole story of the project, and it is why the whatsmeow dependency is pinned to a specific pseudo-version in go.mod rather than floated.

The module targets Go 1.26.0 and pulls in the surrounding stack a service like this needs: gorilla/mux for routing, rs/zerolog for logs, mdp/qrterminal and skip2/go-qrcode for the pairing QR code, go-resty for outbound HTTP, and both modernc.org/sqlite and lib/pq for storage. There is also an AWS SDK v2 dependency with the S3 service client, so media can live outside the host.

The service exposes session, message, user, chat, group, webhook and HMAC configuration endpoints, which is the right surface for an automation layer: connect, act, and receive events.

Connecting a number and scanning the pairing code

The session endpoints are the entry point to everything else, and they cover the full lifecycle of a paired device. You connect, disconnect and log out from WhatsApp, retrieve the connection status, and fetch the QR code to scan. A QR code is how a WhatsApp account is linked to a client library, and the dependency list shows the project renders it in the terminal through qrterminal.

Around that sit the operational endpoints that decide how the linked device presents itself. There is a flag for the connection OS name shown in WhatsApp, and an automatic presence setting that puts the account online after connecting, either available by default or unavailable. The unavailable option is not cosmetic: it preserves phone push notifications, which matters when the same account is also used on a handset and the user does not want a second device to steal attention from the first.

Message endpoints cover text, image, audio, document, template, video, sticker, location, contact and poll messages. The user endpoints answer whether a phone number has WhatsApp, return user information and avatars, and retrieve the full contact list. The chat endpoints set presence such as typing or recording media, mark messages as read, download images out of messages and send reactions. Group endpoints create, delete and list groups, fetch group info and invite links, set participants, and change group photos and names.

Webhooks are the other half, because the endpoint list is only useful if something can react to what arrives. The service can set and get webhooks that fire whenever events or messages are received, and it can be given a global webhook through WUZAPI_GLOBAL_WEBHOOK.

HMAC signing changes with the content type

Webhook security in this project is not a single rule but three, and the difference is easy to get wrong when you write a receiver.

When HMAC is configured, every webhook carries an x-hmac-signature header holding a SHA-256 HMAC signature. What gets signed depends on how you post. For application/json, the signed data is the raw JSON request body, and verification uses the exact JSON received. For application/x-www-form-urlencoded, the signed data is the URL-encoded form string such as key=value&key2=value2, and the receiver has to reconstruct that string from the received parameters. For multipart/form-data, used for file uploads, the signed data is a JSON representation of the form fields with the files excluded, so the receiver creates JSON from the non-file fields.

That last case is the fiddly one. A receiver that verifies the raw multipart body will never match the signature, because the sender hashed a JSON projection of the fields rather than the payload it put on the wire. The documentation's instruction is simple and should be taken literally: always verify signatures before processing webhooks.

Two keys configure this. WUZAPI_GLOBAL_HMAC_KEY is the global signing key and the sample file notes a minimum of 32 characters, while WUZAPI_GLOBAL_ENCRYPTION_KEY is a 32 byte key for AES-256 encryption of sensitive data at rest. Delivery itself can be tuned: WEBHOOK_FORMAT selects json or form as the default, WEBHOOK_RETRY_ENABLED turns retries on, and WEBHOOK_RETRY_COUNT and WEBHOOK_RETRY_DELAY_SECONDS set how many retries and how long between them, with WEBHOOK_ERROR_QUEUE_NAME naming the dead letter queue.

Building it and turning the knobs

The prerequisites are short: Go is required, and Docker is optional for containerization. A source build is one command in the project root:

bash
go build .

There is also a Homebrew tap, which is the shortest path on macOS and Linux:

sh
brew install asternic/wuzapi/wuzapi

The service listens on port 8080 by default, and the runtime flags are where most of the behaviour is decided. The admin token flag, -admintoken, sets the authentication token for admin endpoints and falls back to the .env file when not given. -address sets the bind address, 0.0.0.0 by default, and -port sets the port, also 8080 by default. Logging is chosen with -logtype, either console as the default or json, and -color turns on colored console output. -skipmedia skips downloading media from messages, which is the setting to reach for when you only care about text and your disk or bandwidth is the constraint. -wadebug turns on whatsmeow's own debug output at INFO or DEBUG levels, and it is separate from the service's own logging.

Two flags exist for serving over TLS directly: -sslcertificate for the certificate file and -sslprivatekey for the key file. Otherwise, put a proxy in front of the service.

The two examples the documentation gives look like this:

bash
./wuzapi -logtype=console -color=true
bash
./wuzapi -logtype json

Time zone handling is worth knowing about, since it affects how logs and message timestamps read. Set TZ=America/New_York ./wuzapi ... in your shell, or the same key in your .env file, or in a Docker Compose environment.

Media attachments live in temp files, not in a durable queue

This is the part of the design with the sharpest edges. Incoming event attachments and outgoing document, audio, image, video, sticker and button-image uploads use private temporary files that the service creates and removes automatically. Two settings control this. WUZAPI_MEDIA_TMPDIR defaults to the operating system temporary directory, and WUZAPI_MEDIA_CONCURRENCY defaults to 2, limiting simultaneous media downloads, uploads, decoding, conversions and delivery body preparation. Waiting operations honour cancellation, and webhook retry backoff does not hold a media slot.

The storage advice is specific and worth repeating before you deploy. Use a disk-backed writable directory or a container volume to save memory, because a tmpfs mount still consumes RAM. The path has to be set inside the container and the backing storage mounted there, since setting a host path in .env alone does not mount anything. The directory must support file locks. Allow space for plaintext, encryption or conversion scratch files, and for base64 delivery bodies, which are approximately four thirds of the attachment size each. Slow receivers and pending retries keep their delivery files around longer.

The limitation is stated without hedging: the temporary files are not a durable queue, and they do not recover in-flight messages after a crash. Each process owns a locked private directory, and later starts reclaim abandoned directories without touching another running instance. There are also size-dependent memory costs that the temp files do not remove, in JSON request decoding, image pixel decoding and the final payload buffer. The repository includes a media-memory.md document with measurements and test limits for the attachment memory behaviour, which is the file to read if your workload is media heavy.

Docker Compose with Postgres and RabbitMQ

A container deployment is defined in the repository, and the compose file shows the intended shape. The service is built from a Dockerfile that compiles with CGO enabled on golang 1.26-bookworm and runs on debian:bookworm-slim with ffmpeg, openssl, postgresql-client, curl, netcat-openbsd, tzdata and ca-certificates installed. The entrypoint runs the binary with console logging and color enabled.

The compose file runs the API against Postgres 16 and, optionally, RabbitMQ. The service waits for both dependencies to report healthy before it starts:

yaml
services:
  wuzapi-server:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "${WUZAPI_PORT:-8080}:8080"
    environment:
      - WUZAPI_ADMIN_TOKEN=${WUZAPI_ADMIN_TOKEN}
      - WUZAPI_GLOBAL_ENCRYPTION_KEY=${WUZAPI_GLOBAL_ENCRYPTION_KEY}
      - WUZAPI_GLOBAL_HMAC_KEY=${WUZAPI_GLOBAL_HMAC_KEY:-}
      - WUZAPI_GLOBAL_WEBHOOK=${WUZAPI_GLOBAL_WEBHOOK:-}

RabbitMQ is optional, and when it is used the queue name comes from RABBITMQ_QUEUE, with whatsapp_events as the sample value, and the connection from RABBITMQ_URL. The sample environment also shows WUZAPI_SOCKET_RECOVERY, described as recovering closed sockets after send failures with no automatic message retry, and WUZAPI_WEBHOOK_USE_PROXY for routing webhook deliveries through the per-user proxy.

The local configuration path is the .env file, and the repository ships .env.sample as a template:

bash
cp .env.sample .env

The sample sets WUZAPI_PORT=8080, WUZAPI_ADDRESS=0.0.0.0, the admin token, the two security keys, the global webhook URL, WEBHOOK_FORMAT=json, SESSION_DEVICE_NAME=WuzAPI, WUZAPI_AUTO_PRESENCE=available and WUZAPI_PROXY_POOL_FALLBACK=block, alongside the database variables and the optional RabbitMQ ones. The proxy pool fallback is the setting to understand before deploying: when every entry in the proxy pool is full, block refuses to connect, which is the default, while direct connects without a proxy.

The terms of service warning is part of the design

The project leads with a warning, and it deserves to be quoted rather than paraphrased. Using the software in violation of WhatsApp's terms of service can get your number banned, and the instruction is to be careful and not to use it to send SPAM or anything similar. The project says to use it at your own risk, and to contact a WhatsApp global solution provider and sign up for the WhatsApp Business API service if you need to develop something for commercial purposes.

That last point is also the real alternative, and the difference in approach is fundamental. The WhatsApp Business API is an official channel rather than a client of the consumer protocol. wuzapi is an unofficial client of the consumer protocol, which is why it can be self-hosted, why it needs no business account, and why it can be banned without notice. The upside is control and zero per-message billing for a self-hosted use; the downside is that WhatsApp can change the protocol or enforce terms against the number at any time, and the project says outright that a protocol change will break connections until whatsmeow catches up.

Log verbosity is the last operational detail worth naming. LOG_LEVEL in.env or the process environment accepts trace, debug, info, warn, error, fatal and panic, ignoring case and surrounding whitespace. An unset, empty or invalid value keeps the existing verbosity, numeric values and disabled are not accepted, and the process environment takes precedence over.env even when it is explicitly empty. The filter applies before startup messages and drives the service's zerolog logger, not the whatsmeow output that -wadebug controls.

The licence is MIT, the repository is not archived, and the last push was on 2026-10-19, with releases v1.0.8 on 2026-08-10 and v1.0.9 on 2026-09-17.

Editorial conclusion

Adopt wuzapi if you need a self-hosted WhatsApp integration for a system you control, want multi-device sessions and webhooks with per-content-type HMAC signing, and can accept that a protocol change on WhatsApp's side will break your connections until whatsmeow is updated. Do not adopt it for bulk messaging, cold outreach or anything that breaches WhatsApp's terms, because the project warns in plain language that a number can be banned, and use the official WhatsApp Business API instead if you need a commercial channel. Before you start, set WUZAPI_ADMIN_TOKEN, a 32 byte WUZAPI_GLOBAL_ENCRYPTION_KEY and a WUZAPI_GLOBAL_HMAC_KEY of at least 32 characters, point WUZAPI_MEDIA_TMPDIR at a disk-backed mount with file locks, and decide whether your events go through the optional RabbitMQ queue.

Frequently asked questions

How does wuzapi connect to WhatsApp without a browser?

It is a Go implementation of the @tulir/whatsmeow library that talks directly to WhatsApp's WebSocket servers, so no Puppeteer headless Chrome and no Android emulator are involved. The project describes this as faster and lighter on memory and CPU, and warns that a protocol change on WhatsApp's side will break connections until the library is updated.

How do I install and configure wuzapi?

Build it with go build . in the project root, or install it through Homebrew with brew install asternic/wuzapi/wuzapi. Configuration lives in .env, and the repository ships .env.sample as a template that you copy with cp .env.sample .env before setting WUZAPI_ADMIN_TOKEN, a 32 byte WUZAPI_GLOBAL_ENCRYPTION_KEY and a WUZAPI_GLOBAL_HMAC_KEY of at least 32 characters.

How does wuzapi sign webhooks, and what should my receiver do?

When HMAC is configured, every webhook carries an x-hmac-signature header with a SHA-256 HMAC. For application/json you verify the raw body, for application/x-www-form-urlencoded you reconstruct the URL-encoded form string, and for multipart/form-data you build JSON from the non-file fields, and the documentation says to always verify signatures before processing webhooks.

Can wuzapi be used for commercial messaging?

The project warns that using it in violation of WhatsApp's terms of service can get your number banned and explicitly asks you not to use it for spam. If you need a commercial channel, it directs you to a WhatsApp global solution provider and the official WhatsApp Business API instead.

Where does wuzapi store media attachments?

Incoming and outgoing attachments use private temporary files that are removed when their consumers finish, with the directory taken from WUZAPI_MEDIA_TMPDIR, defaulting to the OS temporary directory, and WUZAPI_MEDIA_CONCURRENCY defaulting to 2. The documentation states these files are not a durable queue and do not recover in-flight messages after a crash, and advises a disk-backed mount with file locks rather than tmpfs.

Official sources

  1. asternic/wuzapi on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/asternic-wuzapi.svg)](https://hysenlabs.com/projects/asternic-wuzapi)

Was this useful?