1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# 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`](rust/media_stickers.rs)
and a runnable sender in [`rust/sendsticker.rs`](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`](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 |
|-------|---|-------|
| url | 1 | upload URL |
| fileSha256 | 2 | sha256 of the `.was` |
| fileEncSha256 | 3 | sha256 of the encrypted blob |
| mediaKey | 4 | 32 random bytes |
| mimetype | 5 | `application/was` |
| height / width | 6 / 7 | 512 / 512 |
| directPath | 8 | from the upload response |
| fileLength | 9 | plaintext length |
| mediaKeyTimestamp | 10 | now |
| isAnimated | 13 | 1 |
| stickerSentTs | 18 | now (ms) |
| isLottie | 21 | 1 |
| premium | 24 | 1 |
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`](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.