Sign in Sign up
Branches
master
Clone over HTTPS

Git asks for your username and password or a token.

Download ZIP

Sending WhatsApp premium (Lottie) stickers

How to send an animated WhatsApp sticker — including a premium one, the kind with the full-bleed secondary animation — and how to send a whole user-created pack, over the WhatsApp multi-device protocol. This documents the method and the exact message layout; the builders are in rust/media_stickers.rs and a runnable sender in rust/sendsticker.rs.

It assumes you already have a linked-device session (a working WhatsApp client). Nothing here forges a signature or pulls a paid asset — it reposts stickers you already have, byte for byte.

The .was file

An animated sticker is a zip (mime application/was):

animation/animation.json                       the sticker — a 512×512 Lottie
animation/animation_secondary.json             the premium effect — a 2048×2048 Lottie, drawn at 2× over it
animation/animation.json.trust_token           ES256 JWT (kid 196) over animation.json's sha256
animation/animation_secondary.json.trust_token  the same, over the secondary
animation/animation.json.overridden_metadata   unsigned: pack id, emojis, accessibility text

Each .trust_token is a JWT whose payload is {"sticker_file_type":"lottie_json","sticker_file_trusted_origin":"whatsapp","sticker_file_sha256":"<b64url sha256 of that one file>"}, signed by Meta with a P‑256 key the client has baked in (key id 196). The token covers only its own file's bytes, and nothing ties the two tokens together — so the main layer of one sticker and the secondary (effect) of another can be combined into a new .was, and both tokens still verify. That is all "mixing" is:

new .was = A/animation.json + B/animation_secondary.json + both original tokens + A's metadata

See python/mix.py. No json is ever rewritten.

What makes it premium

Two independent things:

  1. The effect itself is the signed animation_secondary.json. The client validates it locally (JWT signature + sticker_file_sha256 + trusted_origin) and plays it. It needs no server call.
  2. Whether the client plays it is gated on the sticker being marked premium. For a single sticker that is the stickerMessage.premium field (= 1). It is also carried inside the .was: animation.json's own metadata block has "premium": 1, which the client reads when a sticker is loaded from the tray or a pack. So a sticker whose .was says premium, with a valid signed secondary, animates whether it is sent singly or inside a user-made pack.

Sending a single sticker

Message { stickerMessage = 26 }, fields (WhatsApp Android 2.26.35.71):

field#value
url1upload URL
fileSha2562sha256 of the .was
fileEncSha2563sha256 of the encrypted blob
mediaKey432 random bytes
mimetype5application/was
height / width6 / 7512 / 512
directPath8from the upload response
fileLength9plaintext length
mediaKeyTimestamp10now
isAnimated131
stickerSentTs18now (ms)
isLottie211
premium241

Flow (see sendsticker.rs): encrypt the .was with the image media key (HKDF-SHA256 of a random mediaKey under info "WhatsApp Image Keys", AES‑256‑CBC + a 10‑byte truncated HMAC), request media_conn, POST the encrypted blob to https://<host>/mms/sticker/<token>?auth=…, build the stickerMessage, Signal‑encrypt it to each of the recipient's devices (your own other devices get it wrapped in a deviceSentMessage), and send the <message type="media"> stanza with enc … mediatype="sticker".

Sending a pack

Message { stickerPackMessage = 86 }. The stickers travel as a zip (<sha>.was entries + a tray png), uploaded as mms type sticker-pack, with a 252×252 preview uploaded as thumbnail-sticker-pack under the same media key. The pack message has no premium field — it does not need one; each sticker's premium-ness comes from its own .was (above). Key fields: stickerPackId(1), name(2), publisher(3), stickers(4, each: fileName/isAnimated/emojis/label/isLottie/mimetype), fileLength(5), fileSha256(6), fileEncSha256(7), mediaKey(8), directPath(9), trayIconFileName(14), thumbnail{DirectPath,Sha256,EncSha256,Height,Width}(15–19), imageDataHash(20), stickerPackSize(21), stickerPackOrigin(22 = 2, USER_CREATED). Build the zip + manifest with python/build_pack.py; the same sendsticker path sends it with mediatype="sticker_pack".

Files

  • rust/media_stickers.rs — media encryption, the mms upload, and the stickerMessage / stickerPackMessage builders.
  • rust/sendsticker.rs — a runnable sender (single sticker or --pack <dir>), over a saved session.
  • python/mix.py — combine one sticker's layer with another's effect.
  • python/build_pack.py — assemble a sendable pack folder (zip + tray + 252px preview + manifest) from a list of .was files.