Runbook¶
The commands for routine operations and incidents. The one-time setup is in Getting started. The settings that prevent incidents are in the hardening guide. Every variable is in the configuration reference, every subcommand and exit code in the CLI reference, and every event in observability. This page describes the code at the commit or tag you are reading.
The admin commands below read the admin token from admin.token with
--token-file admin.token. Every admin command needs exactly one token source:
--token-file PATH, --token-stdin or --token-command COMMAND. Without one,
it exits 2. Add --addr URL when the admin listener is not at
http://127.0.0.1:8082.
The distroless image has no admin.token and no shell. In a container, pass
the token on standard input from the host:
Exit codes are listed under exit codes.
Contents¶
- Situations
- Before the first start
- Clients
- Rotating a client token
- Retiring other linked devices
- Rotating the admin token
- Rotating the master key
- Rotating the backup recipient
- Rotating the webhook secret
- Backups
- Restoring a backup
- Audit chain
- Upgrading
- Moving to a new host
- Unpairing and pairing again
- Startup refusals
- Disconnect reasons
- Incidents
Situations¶
| Situation | Section |
|---|---|
| Add, rotate or remove a client | Clients, Rotating a client token |
| Replace the admin token, the master key, the backup recipient or the webhook secret | Admin token, master key, backup recipient, webhook secret |
| Check backups, rehearse or perform a restore | Backups, Restoring a backup |
| Check the audit chain | Audit chain |
| Move to a new release or a new host | Upgrading, Moving to a new host |
serve exits 1 or 2 at start: startup_refused, startup_failed |
Startup refusals |
disconnected, unpaired, a ban |
Disconnect reasons, suspected ban |
pair_rejected, logout_failed |
Pairing rejected |
A token may have leaked, admin_auth_failure |
Token leak, Failed admin authentication |
A send answered 504 or 500, send_outcome_unknown, send_unrecorded |
Uncertain sends |
backup_failed, backup_prune_failed |
Backup failures |
rekey_conflict |
Re-key conflicts |
notify_dropped, notify_failed |
Dropped notifications |
ingest_paused, quarantine, restart_budget, db_lost |
Other incidents |
Before the first start¶
Besides what Getting started sets, decide these before clients depend on the service (environment variables):
WAWARDEN_BACKUP_AGE_RECIPIENT. Optionally alsoWAWARDEN_BACKUP_TIME(UTC,03:00by default) andWAWARDEN_BACKUP_KEEP(14 by default). Without a recipient no backup is taken, and the other two are refused.WAWARDEN_AUDIT_RETENTION_DAYS, 90 by default, 7 to 3650.WAWARDEN_SEND_PER_CLIENT_PER_MINUTE(10 by default and at most) andWAWARDEN_SEND_GLOBAL_PER_HOUR(30 by default, 60 at most). Lower is safer for the account (ban risk).
Keep a copy of keys/master off the host once the first start has created it.
Do the same for keys/retired/ after each
rotation.
Clients¶
Creating a client¶
./wawarden admin chats list --match <part of a name> --token-file admin.token
./wawarden admin clients create --token-file admin.token \
--name <name> --read <chat> [--read <chat> ...] [--expires-days 90]
A chat is an id, a +E.164 number or the ref that admin chats list
prints. A client expires after 90 days unless --expires-days says otherwise,
365 at most.
For a client that sends:
- Add
--write <chat>for each chat it may send to. Each must also be a--readchat. - A client created with
--all-chatscannot write. - Add
--allow-first-contactonly when the agent must write to a direct chat that has never written to the account.
The command prints the token once, on the token: line. Hand it to the
agent's host and nowhere else.
Revoking a client¶
The revocation takes effect on the client's next request. To see a client's chats and state:
Rotating a client token¶
A token cannot be renewed. Before it expires, replace its client with a new
one. admin clients list --token-file admin.token shows each client's id
and expires.
- Create a client with the same chats under a new name. A name stays taken after its client is revoked.
- Put the new token on the agent's host and register it again.
- Check that the agent works with the new token.
- Revoke the old client.
For Claude Code, with the token in WAWARDEN_TOKEN on the agent's host:
./wawarden admin clients create --token-file admin.token \
--name claude-code-2 --read <id, +E.164 number or ref>
read -rs WAWARDEN_TOKEN && export WAWARDEN_TOKEN
claude mcp remove whatsapp
claude mcp add --transport http whatsapp https://<your-host>/mcp --header "Authorization: Bearer $WAWARDEN_TOKEN"
claude mcp get whatsapp
./wawarden admin clients revoke --id <old id> --token-file admin.token
read -rs shows no prompt and echoes nothing. Paste the ww_... value of the
new client's token: line and press Enter. The token then stays out of the
shell's history file.
Revoke the old client only once claude mcp get whatsapp shows the new
registration as connected. Until then the old token still works and can be
registered again. A revocation takes effect on the client's next request.
Registering with Claude Code
says what each --scope writes, and how to keep the token out of files.
Retiring other linked devices¶
An agent may have read WhatsApp through another device linked to the same account before. Once the agent works through WaWarden, log that device out on the owner's phone: Linked devices, select the device, Log out.
A linked device has the account's full access. It is outside every scope, budget and audit row of WaWarden (see trust roots and unlink other devices).
Rotating the admin token¶
The admin token never expires. To replace it:
-
Generate a new token and its SHA-256 into files of mode
0600, and delete the full output. The shell creates the files, so set theumaskfirst: -
Put the new hash from
new.sha256inWAWARDEN_ADMIN_TOKEN_SHA256, or in the file thatWAWARDEN_ADMIN_TOKEN_SHA256_FILEnames. - Put the new token from
new.tokenin the file the CLI reads. - Restart the service. The hash is read only at start.
Rotating the master key¶
keys rotate works offline, on a stopped service
(keys rotate):
- Make sure a recent backup exists and you hold a copy of
keys/master. - Stop the service.
- As the service's user, run
wawarden keys rotate --data-dir <data dir>. With the image, rundocker run --rm --mount type=volume,src=<volume>,dst=/data <image> keys rotate --data-dir /data. It prints the previous and the new key id.service_runningmeans something still holds the archive. - Start the service. It logs
keys_loadedwith the number of retired keys. It also logsaudit_key_changedatINFO, withprevious_keyretired, until the first audit row is written under the new key. - Copy
keys/masterandkeys/retired/off the host.
What changes:
- Log pseudonyms and new message references change.
- Cursors and references issued before keep working while their key stays in
keys/retired/. - Nothing deletes a retired key.
audit verifyneeds every key that itskey idsline lists.- The audit retention needs them too: it removes no row it cannot verify.
An audit_key_changed at WARN, with previous_key missing, means the
master key was replaced without keeping the old one. Restore your copy of it
into keys/retired/ under its key id, as keys rotate would have.
Rotating the backup recipient¶
The service holds one age recipient, WAWARDEN_BACKUP_AGE_RECIPIENT, and reads
it only at start. To move backups to a new identity:
- On the machine that keeps the identity, create a new key pair:
age-keygen -o backup-identity-2.txt. It prints the newage1...public key. - Set
WAWARDEN_BACKUP_AGE_RECIPIENTto the new public key and restart the service. - The next backup is encrypted to the new recipient. That is the nightly one, or one at once when the start finds a backup owed. Check it with the new identity as in Checking a backup.
- Keep the old identity while any backup encrypted to it exists. Retention
deletes the old backups in
backups/afterWAWARDEN_BACKUP_KEEPnew ones. Copies off the host and storage snapshots keep theirs for as long as you keep them.
If the old identity leaked, delete the old backups by hand once a new backup
has been verified: in backups/, off the host and in snapshots. A backup holds
the whole archive and the linked device's keys. If a backup leaked together
with its identity, also unpair and pair again.
Rotating the webhook secret¶
The webhook secret is the content of the file that
WAWARDEN_NOTIFY_SECRET_FILE names, read only at start. Each delivery carries
one v1= signature, and a receiver accepts a request when any comma-separated
value of WaWarden-Signature matches
(webhook).
- Create the new secret, at least 32 bytes, readable only by the service's
user:
(umask 077; openssl rand -hex 32 > notify.secret.new). - Make the receiver accept a signature under either secret.
- Put the new secret in the file the variable names, or point the variable at the new file. In a container, the file is mounted read-only; replace it on the host.
- Restart the service. Events still queued at the stop are dropped and
reported in
notify_dropped; they stay on standard output. - Check that a new event verifies with the new secret. An
admin reconnect --token-file admin.tokenwhile the engine is connected answersalready_connected, exits5, and posts anadmin_mutationevent. - Remove the old secret from the receiver.
The start refuses a secret file with group write access or any access for
others (notify_secret_permissions), and a secret shorter than 32 bytes
(notify_secret_invalid).
Backups¶
Checking a backup¶
Copy the newest backups/<UTC time>.age off the host to where the age
identity is kept, and run:
set -o pipefail
mkdir -m 700 verify
age --decrypt -i backup-identity.txt <UTC time>.age |
wawarden backup verify --work-dir verify -
It prints counts and verified: yes and exits 0. Other results:
1: the file cannot be decrypted or read to its end. The file is damaged or truncated, or the identity is wrong.6: the decrypted stream is not a complete, sound backup (backup verify).
Compare its counts with admin status --token-file admin.token of the live
service.
Restore drill¶
Rehearse a restore away from the deployment, with a copy of a backup and the
identity kept off the service's host. backup verify proves three things: the
identity opens the backup, both databases pass their integrity checks, and
this build reads their schemas.
- Decrypt and extract: the pipeline above with
--extract-to restore. It leavesrestore/archive.dbandrestore/session.db. -
Check the audit chain of the extracted archive with your copies of the keys:
wawarden audit verify --db restore/archive.db --master-key-file master \ --retired-keys-dir retired --log heads.logheads.logis the captured output cut at the backup's time, as in Audit chain. An uncut capture also holds the heads shipped after the backup. The copy lacks them, so they would count as missing. 3. To startserveon it, use a scratch data directory of mode0700that holds only the restoredarchive.db. The missingsession.dbis created empty, so the engine stays unpaired and makes no connection. Never start a restoredsession.dbwhile the original service runs. 4. Delete the decrypted files when done. They hold the whole archive and the linked device's keys.
Restoring a backup¶
There is no restore command. A restore is done by hand, into a stopped service. The backups reference describes what a backup holds.
- Stop the service. Make sure that no process or task still runs on the data directory.
- Verify the backup with
backup verify --extract-to, as in the restore drill, on a machine apart from the host. -
Replace
archive.dbandsession.dbin the data directory with the two files of that one backup, never one from another. They are two points in time, the archive's first.- Give them the service's user as owner and mode
0600. - Keep the data directory at
0700. - Delete any
archive.db-journalandsession.db-journal. - Never restore
backups/itself.
- Give them the service's user as owner and mode
-
Restore your own copy of
keys/master, which no backup holds. Restorekeys/retired/too whenkeys rotateran. Without the master key the service creates a new one. Pseudonyms, cursors and message references then change, and audit rows written under the old key verify asunknown_key(see Master key).audit verifyof the restored archive needs every key whose id itskey idsline lists. - Note the current UTC time, then start the service.
What a restore rolls back¶
A restore takes the service back to the time of the backup:
- Clients. Clients revoked after the backup are active again, with their old tokens. Clients created after it are gone.
- Sends. The send budgets and the record of sends already made roll back with the archive. A send retried after the restore can go out again.
- Audit rows. Rows written after the backup are missing. The rows written after the restart take their ids with other chain heads, so the heads shipped between the backup and the restart can never match.
- Messages. Messages received after the backup are missing. The text of messages revoked or edited after it comes back. So does the text of messages that expired after it, until the expiry sweep removes it again within a minute of the start.
- The linked device. The device store holds the device's keys and sessions as of the backup. The protocol library logs in with the stored keys and does not compare them with WhatsApp's view. If the device was unlinked since, the service is logged out and must be paired again. WaWarden does not know how WhatsApp's servers and contacts treat sessions and pre-key numbers that went back in time; only the real service shows it.
Before giving agents access again¶
- Read the
admin_mutationevents since the backup. - Revoke again every client revoked since the backup.
- Keep clients that can send stopped until you are sure what was sent.
- Keep the capture of standard output from before the restore. It is the only record of the requests whose audit rows were lost.
- Leave the heads shipped in between out of every later
--log, as Audit chain shows. Otherwiseaudit verifycounts them as missing and exits6. That lasts until the retention removes the first rows written after the restart, aboutWAWARDEN_AUDIT_RETENTION_DAYSlater.
Never start two instances from one session.db, for example a restored copy
while the original still runs. Both would act as the same linked device and
replace each other (replaced).
Audit chain¶
Each request's audit line on standard output carries the chain head. Capture
standard output. From time to time, run audit verify on the archive of a
fresh backup, as in step 2 of the restore drill, with --log
pointing at the capture cut at the backup's time
(audit verify).
Every head shipped after the backup is missing from its copy. Keep only the
audit lines whose ts is at least a minute earlier than the UTC time in the
backup's file name: a request's row can be written a few seconds after its
ts. With jq, for 20261011T030000Z.age:
jq -cR 'fromjson? | select(.event == "audit" and .ts < "2026-10-11T02:59")' \
captured.log > heads.log
After a restore, also leave out the heads shipped in the
restore window, and nothing more. The window runs from a minute before the
restored backup's time to the time noted just before the restored service
started. Rows removed before or after that window are then still found. For a
restore of 20261011T030000Z.age started at 09:15 UTC that day, checked
against the backup 20261012T030000Z.age:
jq -cR 'fromjson? | select(.event == "audit" and .ts < "2026-10-12T02:59"
and (.ts < "2026-10-11T02:59" or .ts >= "2026-10-11T09:15"))' \
captured.log > heads.log
| Result | What to do |
|---|---|
none |
Nothing. |
unknown_key |
A row was written under a key you did not pass. Find the copy of that master key, or of the retired keys, and pass it. |
anchor_invalid |
The anchor's key is missing, or the anchor was altered. Pass every key; if it still fails, treat it as tampering. |
hmac_mismatch, id_gap, retired_key |
Rows were altered, removed or reordered, or the copy is not what you think. Treat it as an incident, keep the files, and compare with older backups. |
| Shipped heads missing | The same, when the capture is cut at the backup's time and the window of every restore is left out. |
Rows older than WAWARDEN_AUDIT_RETENTION_DAYS are removed each night after
the backup, behind a signed anchor, and the service logs audit_pruned.
An audit_prune_refused with chain_mismatch means the anchor, or a row due
for removal, did not verify under the master key and the retired keys. The
removal stopped at the anchor before that row. Run audit verify on a fresh
backup as above, with the service's keys/master and keys/retired/:
unknown_keythere means a key is missing fromkeys/retired/. Restore it as in Rotating the master key.- Treat any other result as the table says.
Upgrading¶
A release brings the archive and the device store forward to its schema. A
release refuses an archive written by a newer one (archive_schema_newer). So
going back means restoring the backup taken before the
upgrade into the older release.
Before any upgrade¶
- Read the release notes, and verify the release as RELEASING.md describes.
- When moving from a release that could not send to one that can, list the
clients first:
./wawarden admin clients list --token-file admin.token. A client created with write chats becomes a live sender on the upgrade. Revoke those that should not send. - Make sure a recent backup succeeded (
backup_done) and is copied off the host, with the copies ofkeys/masterandkeys/retired/.
Container¶
- Stop the container with at least 10 seconds of grace
(
docker stop --time 15 <container>, or--stop-timeout 15when it was created). - Start the new release by digest, with the same volume and settings.
-
Check the new release:
docker exec <container> /wawarden versiondocker exec <container> /wawarden healthcheckdocker exec -i <container> /wawarden admin status --token-stdin < admin.token
-
Watch the events of the first minutes.
Binary and systemd¶
For the unit in the hardening guide, which runs
/usr/local/bin/wawarden and gives it TimeoutStopSec=15:
- Download
wawarden_<version>_linux_<arch>andSHA256SUMSfrom the release, and verify both as RELEASING.md describes. - Install the new binary next to the old one:
sudo install -m 0755 wawarden_<version>_linux_<arch> /usr/local/bin/wawarden.new. - Stop the service:
sudo systemctl stop wawarden. -
Keep the old binary for a rollback, and move the new one into place:
-
Start the service:
sudo systemctl start wawarden. -
Check the new release:
wawarden version./wawarden admin status --token-file admin.tokenjournalctl -u wawardenfor the first minutes of events
-
Delete
wawarden.oldonce you no longer need it. An older release refuses an archive that a newer one brought forward, so a rollback needs the backup from before the upgrade too.
Moving to a new host¶
Move the whole data directory of a stopped service. It holds everything the
service keeps: the archive, the device store, keys/master and
keys/retired/. Backups do not hold the keys, so copying the directory loses
less than restoring a backup.
- Prepare the new host as in Getting started and the hardening guide. Give it the same variables, the admin token hash, the backup recipient and the webhook secret. Do not start it yet.
- Make sure a recent backup succeeded and is copied off the old host.
- Stop the service on the old host, and keep it from starting again
(
sudo systemctl disable --now wawarden, ordocker update --restart=noanddocker stop). - Copy the data directory to the new host, including any
-journalfiles andkeys/retired/.backups/is optional. -
Make the copy pass the start's checks:
- every directory and file is owned by the new service's user
(
65532:65532for the image); - every directory, the data directory included, has mode
0700; - every file has mode
0600.
- every directory and file is owned by the new service's user
(
-
Set
WAWARDEN_DATA_DIRto the new path, or mount the copy on/data. SetWAWARDEN_STORAGE_PROFILE=nfswhen the new directory is on a network filesystem. - Start the service on the new host. Check that
session_openedshows"paired":trueand thatadmin status --token-file admin.tokenreachesconnected. - Point the agents and the proxy at the new host. Client tokens do not change.
- Once the new host works, delete the data directory on the old host. It holds the linked device's keys.
Never start both copies. Two services that hold the same device replace each
other's connection (replaced).
Unpairing and pairing again¶
There is no admin command that unpairs. To unlink WaWarden, log the device out on the owner's phone: Linked devices, select the device, Log out.
WhatsApp logs the device out at once while it is connected, or at its next connection. Then:
- the engine reports
disconnectedwith reasonlogged_out, thenunpaired; admin statusshowspaired: false, and thewawarden_pairedandwawarden_connectedgauges fall to0;- the device is deleted from
session.db; - the archive keeps everything stored so far, and nothing more arrives;
- after a restart, the engine starts
unpaired.
To pair again, for the same account:
Enter the code on the owner's phone as in Getting started.
Pairing is limited to 3 attempts an hour, and only the number in
WAWARDEN_OWNER_PHONE can be linked. The new device gets its own history sync
and, once that has settled, its own backup.
After a logout nobody caused, find out why before pairing again: see suspected ban.
Startup refusals¶
serve checks its whole configuration, the master key, the archive and the
device store before it opens any socket. A start that stops writes one JSON
line on standard output and exits with a code
(exit codes):
| Exit code | Event | What it means |
|---|---|---|
2 |
startup_refused |
A check failed. reason gives the code, and error names the variable or the file in the data directory. |
1 |
startup_failed |
The configuration was accepted, but the archive or the device store could not be opened or brought up to date, a lock was not acquired within five minutes, or a listener could not be opened. |
Read the line in the service's output: journalctl -u wawarden under systemd,
docker logs <container> for a container. Then:
startup_refused. Look upreasonin Startup refusals, which gives every code in the order the checks run. Fix that one cause and start again; the next start can stop at a later check.startup_failedwith a lock. Another process holds the data directory, such as a second service or akeys rotate. Only one process may use it at a time.startup_failedwith a listener. Another process holds the address, or the address cannot be bound.startup_failedwith the device store. A store that a newer protocol library wrote, and that does not declare itself compatible, stops the start. This happens after going back to an older release: see Upgrading.
Common refusals:
reason |
Cause |
|---|---|
unknown_variable |
A WAWARDEN_ variable the service does not know, such as WAWARDEN_TOKEN, which belongs on the agent's host only. |
plaintext_admin_token |
WAWARDEN_ADMIN_TOKEN is set. Give the service the hash only. |
running_as_root |
The service runs as root. Run it as its own user. |
data_dir_foreign_owner, data_dir_permissions |
The data directory is not owned by the service's user, or its mode is not exactly 0700. |
archive_schema_newer |
The archive was written by a newer release. |
Disconnect reasons¶
This is the canonical list of the engine's disconnect reasons. A disconnection
never stops the process. /healthz still answers 200, and reads keep working
from the archive. Sends answer 503 not_connected.
The engine retries an ordinary drop by itself, waiting up to 5 minutes between
attempts; its state is then connecting. It stays disconnected for the
reasons below until you act. admin status shows state and reason. Every
reason but shutdown is also reported as the disconnected event.
reason |
What happened | What to do |
|---|---|---|
outdated |
No protocol version could be fetched from web.whatsapp.com at the start. Or WhatsApp reported the client as outdated, and no newer version could be fetched. |
Check outbound access, then admin reconnect, which fetches again, or restart. After WhatsApp reported the client as outdated, only a strictly newer version helps: try again later, or upgrade WaWarden. |
replaced |
Another linked device or protocol client took over the session, such as a second service started from a copy of session.db. |
Stop the other one, then admin reconnect. |
logged_out |
The device was logged out, from the phone or by WhatsApp. It is deleted, and paired is false. |
Find out why (suspected ban), then pair again. |
temporary_ban |
WhatsApp banned the account temporarily. | Follow suspected ban. Wait for the ban to end, then admin reconnect. |
cat_refresh |
A connection token could not be refreshed. | admin reconnect. |
connect_failure |
WhatsApp refused the connection. | Look at the phone (suspected ban), then admin reconnect. |
restart_budget |
The service started more than 5 times in 10 minutes, so the engine did not connect. | Find out from the logs why it restarts. Then admin reconnect, or admin pair when no device is stored. Or restart once the 10 minutes have passed. |
owner_mismatch |
The stored device's number is not WAWARDEN_OWNER_PHONE. |
See owner mismatch. admin reconnect is refused. |
shutdown |
The service is stopping. | Nothing. |
The commands, with their token flag:
admin reconnect answers already_connected while the engine is connected,
and not_paired when no device is stored. Both exit 5. See
engine states for the engine's behaviour.
Owner mismatch¶
The engine checks the stored device's number against WAWARDEN_OWNER_PHONE
before every connection. When they differ, it stays disconnected with
owner_mismatch and does not log the device out. The cause can be the
configuration or another account:
WAWARDEN_OWNER_PHONEis wrong. Correct it and restart.- The device belongs to another account, as after a failed logout of a
rejected device:
- Stop the service.
- Delete
session.dbandsession.db-journalfrom the data directory. - Remove the device from that account's Linked devices.
- Start the service and pair again.
Incidents¶
Engine disconnected¶
admin status --token-file admin.token shows state and reason. Find the
reason in Disconnect reasons, which gives the action for
each.
Suspected ban¶
Signs: disconnected with temporary_ban or connect_failure, or a
logged_out that nobody caused from the phone.
- Revoke every client that can send:
./wawarden admin clients revoke --id <id> --token-file admin.token. - Look at the owner's phone and in WhatsApp for a notice of a restriction or ban. WaWarden does not report WhatsApp's reason.
-
Check what was sent before:
- the audit lines with the actions
rest.sendandmcp.send_message; wawarden_sends_rejected_totalby reason;- the archive's messages with
origingateway:<client id>, as a read client sees them, which also hold a send whose audit row the busy archive could not take; - the chats themselves on the phone.
- the audit lines with the actions
-
For a temporary ban, wait for its end, then
./wawarden admin reconnect --token-file admin.token. After a logout, pair again only once the phone works normally. Pairing is limited to 3 attempts an hour. - Recreate clients with write chats, narrower than before.
Ban risk explains the causes.
Pairing rejected¶
Only the account whose number is WAWARDEN_OWNER_PHONE can be linked. The
pair_rejected event says another account tried, with stage:
before_save: the code was entered on another account's phone. Nothing was stored, and the engine staysunpaired. Check the number inWAWARDEN_OWNER_PHONE, then runadmin pairagain and enter the code on the owner's phone.after_pairing: pairing completed for another number. The engine staysunpairedand logs the new device out.
A failed logout is reported as logout_failed, with attempt and
error_type. The engine tries again with the reconnection delays until it
succeeds or the device is gone. Until then, the engine never connects the
device, and pairing stays refused (already_paired). Check the outbound access
to WhatsApp. When the service restarts with the device still stored, it starts
in owner_mismatch; follow the steps there.
Pairing starts only from the admin route. A pair_rejected that you did not
cause means someone else used the admin token: see Token leak.
Token leak¶
A client token.
- Revoke the client:
./wawarden admin clients revoke --id <id> --token-file admin.token. It fails from its next request. - Create a replacement with a new name.
- Read the client's audit lines since the leak to see what it read. For a client with write chats, also see what it sent. The send limits and the first-contact rule bound what it could send, and the phone shows the messages as the owner's.
The admin token.
- Replace it as in Rotating the admin token.
- Run
./wawarden admin clients list --token-file admin.token, and revoke every client you did not create. - Read the
admin_mutationevents since the leak: pairing, reconnects, backfills and client changes. - Check Linked devices on the phone.
Failed admin authentication¶
admin_auth_failure reports failed authentications on the admin listener, with
their count. The first failure is reported at once. Further failures within
the next minute are reported together once the minute has passed.
- When you caused it, with a wrong or old token file, fix the file.
- Otherwise, something that can reach the admin listener holds a wrong token. Check who can reach it (gate the admin listener). If you suspect the token itself leaked, follow Token leak.
The listener has one failure budget for all callers. While it is spent, a wrong
token is answered 429 too_many_requests instead of 401, and an admin
command exits 4. A valid token is still accepted.
Uncertain sends¶
A 504 send_outcome_unknown means the message may have reached WhatsApp.
The service never sends it again, and every retry with the same key answers
504. The event send_outcome_unknown gives the reason: ack_timeout,
lease_expired or restart. Read the chat, through the API or on the phone,
before anyone sends anything else to it.
send_unrecorded means a send's outcome could not be stored, or the key of a
send that never left could not be freed. The key answers 409 until its
60-second lease ends, and 504 afterwards. Treat it like a 504, and look for
the cause of the failed write, such as a full disk.
A 502 send_rejected is final: WhatsApp refused the message, or the
recipient cannot be reached. Retrying under the same key answers the same.
Backup failures¶
backup_failed carries a reason (backups). The
service retries after 15 minutes, then with waits that double up to 4 hours.
space: free space or grow the volume. Old backups can be deleted by hand.directory,output,rename,cleanup: check thatbackups/andbackups/tmp/are directories of the service's user with mode0700.exists: a file already has the backup's name; look at the clock.archive,session,manifest,encrypt,sync: usually the disk. Read the system's logs.cancelled: the service was stopping; nothing to do.
backup_prune_failed with unexpected_entry means backups/ holds something
the service did not write. Until it is removed, no old backup is deleted.
directory, remove and sync point at permissions or the disk.
Re-key conflicts¶
WhatsApp can move a direct chat from a phone number to a LID. The engine
refused such a mapping when rekey_conflict is reported, with conflict:
conflict |
The mapping was refused because |
|---|---|
mapping_contradicts |
It contradicts a mapping already learned. |
both_chats_have_messages |
It would merge two chats that both hold messages. |
message_collision |
Re-keying the sender would give two messages of one chat the same id. |
scoped_chat |
It would widen what a client reads: a client names only one of the two identities while the other holds messages, or the merge would remove a chat a client names. |
The message that carried the mapping is still applied. The number and the LID
stay apart, and no client's scope changes. The event is reported the first
time a mapping is refused since the start; wawarden_rekey_conflicts_total
counts every refusal.
For scoped_chat, an expired client counts until it is revoked. Revoke expired
clients whose chats hold a mapping back
(./wawarden admin clients list --token-file admin.token shows them). See
ingest for the rules.
Dropped notifications¶
Every notification event is written on standard output first. The webhook only copies it, so a dropped post loses no event from the output.
notify_failed(attempts,reason,status): a post failed after its last attempt, got a final answer, or met a refused destination.reasonisdestination_refused,http_statusornetwork_error. Check the receiver and the address thatWAWARDEN_NOTIFY_URLresolves to.notify_dropped(count): at shutdown, the webhook droppedcountevents it had not delivered within the grace period, andserveexits1. Read those events from standard output.
wawarden_notify_dropped_total counts the drops by reason: queue_full,
failed and shutdown. See webhook.
Other incidents¶
ingest_paused(free_bytes,floor_bytes): the data directory fell belowWAWARDEN_MIN_FREE_BYTES. Messages that arrive while ingest is paused are lost, not delayed (delivery). Free space or grow the volume. Ingest resumes at 1.25 times the floor and logsingest_resumed. The service never deletes backups to make room.quarantine(queue:inboxorhistory): an item failed three times and was set aside;admin statuscounts it. Read the log lines around it. If a crafted message could cause it, report it as SECURITY.md describes.restart_budget: see Disconnect reasons.db_lost: the connection holding a database's lock is gone, and/healthzanswers503. Restart the service, and find out what removed or locked the file.-
A history gap: ask the phone for the 50 messages before the oldest one the archive holds:
At most one request every 5 minutes and 20 a day.
backfill_quotaandbackfill_no_anchorexit5.