Stickers & Custom Emoji
Read a space's sticker and custom emoji packs, create packs of your own and fill them.
Files go up once through IFiles or inline with the request,
and changes arrive in real time via the Expressions intent.
Packs & Items #
Stickers and custom emoji are called expressions. They live in packs that belong to a space,
and every pack holds one kind of item:
:name:, and as a reaction.
Each item has a format:
static (WEBP),
lottie (TGS, gzipped Lottie JSON) or
video (WEBM, VP9).
Example — a pack with one item (BotExpressionPackV1)
{
"packId": "7c9e6679-...",
"spaceId": "aaaabbbb-...",
"kind": "sticker",
"title": "Hot Cherry",
"slug": "hotcherry",
"coverItemId": null,
"sortOrder": 0,
"version": 2,
"creatorId": "d3b07384-...",
"createdByBot": true,
"items": [
{
"itemId": "9b2f4a1e-...",
"packId": "7c9e6679-...",
"spaceId": "aaaabbbb-...",
"kind": "sticker",
"format": "lottie",
"name": "Hot Cherry 1",
"url": ".../files/0d6c1f2e-...",
"width": 512,
"height": 512,
"fileSize": 38112,
"emoji": ["🍒"],
"keywords": ["cherry"],
"textColor": false,
"sortOrder": 0,
"creatorId": "d3b07384-...",
"createdByBot": true
}
]
} Files travel as URLs. An item carries its file as url.
createdByBot is set when a bot account added the pack or item;
creatorId is that account's user ID.
Formats & Limits #
The server reads the file's own signature, not its name or Content-Type, and checks it when an item is added.
A file that breaks a rule is refused with invalid_format; one over its size with
too_large. Sizes are in KB of 1024 bytes.
Forbidden in Lottie / TGS
A file that uses any of these is refused with invalid_format:
- Expressions (script on an animated property)
- 3D layers (
ddd), on the animation or a layer - Solid, image, text, audio and camera layers
- Masks, layer effects, auto-orient, time remapping and time stretching
- Image assets, linked or embedded
- Gradient strokes, repeaters, merge paths and star / polygon shapes
Animated items. The server renders the preview of every lottie and
video item itself; you send the file and nothing else.
When it cannot render right now, AddItem answers
503 render_unavailable and adds nothing; send the same request again later.
Names & metadata
[a-z0-9_], 2–32 characters, unique among all the space's emoji — it is the :name: people type. A taken name returns 409 name_taken. false. [a-z0-9_], 1–64 characters, unique in the space across both kinds. Set once: the Bot API cannot change it. Quotas #
A space holds a limited number of packs and items. The item slots grow with the space's boost level;
a full space, a full pack or too many packs refuse the next addition with 422 quota_exceeded.
On top of the slots: at most 10 packs per space (stickers and emoji together),
120 stickers per sticker pack and 200 emoji per emoji pack.
These are the defaults; GetQuota tells you what applies to the space right now.
Request
GET /IExpressions/v1/GetQuota?spaceId=aaaabbbb-...
Authorization: Bot YOUR_TOKEN Response (BotQuotaV1)
{
"packs": { "used": 1, "max": 10 },
"stickers": { "used": 4, "max": 18 },
"emoji": { "used": 0, "max": 120 },
"itemsPerPack": { "sticker": 120, "emoji": 200 },
"boostLevel": 1
} A space without boosts holds 6 stickers. Read GetQuota before adding many items
and tell the user how many fit, rather than stopping part way through with quota_exceeded.
Who May Change What #
Reading — List, GetPack,
GetQuota — only needs the bot to be a member of the space.
Writing — CreatePack, UpdatePack,
AddItem, UpdateItem — needs the
CreateExpressions permission, and even then a bot is held to three rules:
Only its own. A bot updates only the packs and items it created (creatorId is the bot);
updating a person's pack or item returns 403 insufficient_permissions.
Adding is not limited that way: AddItem takes any pack of the space, and the item it makes is the bot's.
Never delete, never reorder. There are no routes for it, and the server refuses a bot either way.
Removing a pack or item is left to members with ManageExpressions.
ManageExpressions adds nothing. For people it opens everyone's packs; granting it to a bot changes none of the above.
CreateExpressions is an ordinary entitlement: add it to the bot's
required entitlements on the app's Entitlements tab in the
Developer Console, like any other.
See Permissions & Intents.
Files #
AddItem is a multipart/form-data request.
Every field is a text part; a list is the field repeated once per value (or one field holding a JSON array); a boolean is
true or false.
The file field takes a file in one of three ways:
clip). A name that matches no part returns 400 invalid_request. fileId of an earlier IFiles/Upload. Unknown or expired returns 404 not_found.
An AddItem request carries at most 5 MiB in total; more is refused with 413 too_large.
Every item gets its own copy of the file, so the same file can make any number of items.
Uploading once with IFiles
POST /IFiles/v1/Upload takes a file part
and a purpose, and returns a fileId
that AddItem takes in place of the part — in any space, any number of times, for 24 hours.
Useful when one file goes into several spaces, or when you want uploads and additions apart.
Upload
curl -X POST \
-H "Authorization: Bot YOUR_TOKEN" \
-F "purpose=sticker" \
-F "file=@wave.webp" \
https://gateway.argon.zone/IFiles/v1/Upload Response (BotFileV1)
{
"fileId": "5f1d0c3a-...",
"size": 23120,
"contentType": "image/webp",
"url": ".../files/5f1d0c3a-...",
"fileName": null
} AddItem — see Files & Attachments. Upload checks the type and the size, nothing more. Dimensions, duration, frame rate and Lottie features are checked when
AddItem uses the file, against the kind of the pack it goes into.
A bot holds at most 100 unused uploads at a time — the next returns 422 quota_exceeded.
An upload stops counting once an item is made from it, and is deleted 24 hours after it was sent.
GET /IFiles/v1/Get?fileId=… describes a file the bot uploaded, or any sticker or emoji file:
its size, contentType and a download
url. Anything else is 404 not_found.
Creating Packs & Adding Items #
POST /IExpressions/v1/CreatePack takes JSON: the space, the
kind, a title and a
slug. It answers with the pack (BotExpressionPackV1);
its packId is what AddItem needs.
CreatePack
curl -X POST \
-H "Authorization: Bot YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"spaceId":"aaaabbbb-...","kind":"sticker","title":"Hot Cherry","slug":"hotcherry"}' \
https://gateway.argon.zone/IExpressions/v1/CreatePack CreatePack is safe to repeat. Called again by the same bot with the same slug and kind, it returns the pack it made — with its items —
and changes nothing. The same slug held by someone else's pack, or by your pack of the other kind,
is 409 name_taken.
POST /IExpressions/v1/AddItem adds one sticker or emoji to a pack, with the file given as
described in Files. It answers with the new item (BotExpressionItemV1).
More associated emoji or keywords: repeat the field (-F "emoji=🍒" -F "emoji=❤️"), or send one field holding a JSON array.
AddItem
curl -X POST \
-H "Authorization: Bot YOUR_TOKEN" \
-F "spaceId=aaaabbbb-..." \
-F "packId=7c9e6679-..." \
-F "name=Hot Cherry 1" \
-F "emoji=🍒" \
-F "keywords=cherry" \
-F "file=@cherry.tgs" \
https://gateway.argon.zone/IExpressions/v1/AddItem AddItem is not idempotent: each call adds a new item, even with the same file,
so keeping a re-import from adding duplicates is the bot's own concern.
Updating Packs & Items #
PATCH /IExpressions/v1/UpdatePack changes the title
or the coverItemId (an item of that pack) of a pack the bot made.
PATCH /IExpressions/v1/UpdateItem changes the name,
emoji, keywords or
textColor of an item the bot made. Both are JSON. A field left out — or null — is kept;
an empty list clears emoji or keywords.
A list you send replaces the old one, so send the whole list.
UpdatePack
curl -X PATCH \
-H "Authorization: Bot YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"spaceId":"aaaabbbb-...","packId":"7c9e6679-...","coverItemId":"9b2f4a1e-..."}' \
https://gateway.argon.zone/IExpressions/v1/UpdatePack UpdateItem
curl -X PATCH \
-H "Authorization: Bot YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"spaceId":"aaaabbbb-...","itemId":"9b2f4a1e-...","keywords":["cherry","love"]}' \
https://gateway.argon.zone/IExpressions/v1/UpdateItem
Both answer with the pack or item as it now is. A request that changes nothing succeeds without counting against the space's budget.
The file of an item cannot be replaced: add a new item, and ask a member with ManageExpressions to remove the old one.
Errors #
Branch on the error code (see Error Handling).
Some refusals are about one file and the next one may pass; others mean every further write fails the same way.
attach:// names no part. CreateExpressions in this space, or, on an update, the pack or item is not the bot's. fileId is unknown or older than 24 hours. CreatePack, the slug). Pick another. GetQuota says which. IExpressions 60/min, IFiles 30/min. Wait Retry-After seconds and send the same request again. Retry-After: 60; wait and send it again. Two different 429s. rate_limited is your bot's own limit, across all spaces.
space_rate_limited is the space's budget for sticker and emoji changes, shared by every bot in it
(people have a budget of their own). An AddItem refused for its file —
invalid_format, too_large or
content_rejected — has already used one change of that budget. See Rate Limits.
Reading & the Version Token #
GET /IExpressions/v1/List returns every pack of the space with its items, sorted by kind and order, and a
version token that changes whenever any pack or item does.
Keep the token and pass it back as known: while nothing changed, packs
comes back null and costs nothing to send.
Request
GET /IExpressions/v1/List?spaceId=aaaabbbb-...&known=3F9A0C1B7E22D4A5
Authorization: Bot YOUR_TOKEN Response — still current
{
"version": "3F9A0C1B7E22D4A5",
"packs": null
}
The token is opaque: compare it for equality, never parse it. For one pack, GET /IExpressions/v1/GetPack
takes packId or slug (one of them is required) and answers
404 not_found for a pack the space does not have.
Real-time Events #
The Expressions intent (bit 14, value 16384) delivers
expressionsUpdate whenever a pack or item of a space the bot is in changes — including the bot's own changes.
It is one of the default intents a stream gets when it names none.
List would return now. delta only if it equals the token you hold. null for a change V1 has no shape for — re-read List. pack.items is empty — keep the items you hold. ordered is every pack ID of that kind, in the new order. ordered is every item ID of the pack, in the new order. SSE example — event: expressionsUpdate, data:
{
"spaceId": "aaaabbbb-...",
"version": "3F9A0C1B7E22D4A5",
"baseVersion": "C04D19E8A7B3F210",
"delta": {
"type": "itemUpserted",
"packId": "7c9e6679-...",
"itemId": "9b2f4a1e-...",
"item": {
"itemId": "9b2f4a1e-...",
"packId": "7c9e6679-...",
"kind": "sticker",
"format": "lottie",
"name": "Hot Cherry 1",
"url": ".../files/0d6c1f2e-...",
"createdByBot": true
}
}
} The rule: if baseVersion is the token you hold and delta is not
null, apply the delta and hold version.
Otherwise — you missed an event, or just started — call List with known and take what it returns.
Fields a delta type does not use are empty; ignore them.
Keeping a space's packs in sync (TypeScript)
const API = "https://gateway.argon.zone";
const AUTH = { Authorization: "Bot " + process.env.ARGON_TOKEN };
// Per space: the version token you hold and the packs it stands for.
const spaces = new Map<string, { version: string; packs: any[] }>();
async function reload(spaceId: string) {
const held = spaces.get(spaceId);
const known = held ? "&known=" + encodeURIComponent(held.version) : "";
const res = await fetch(API + "/IExpressions/v1/List?spaceId=" + spaceId + known, { headers: AUTH });
const list = await res.json();
// packs is null when the version you sent is still current.
spaces.set(spaceId, { version: list.version, packs: list.packs ?? held!.packs });
}
// es: the bot's event stream (see Real-time Events).
es.addEventListener("expressionsUpdate", (e) => {
const { spaceId, version, baseVersion, delta } = JSON.parse(e.data);
const held = spaces.get(spaceId);
// Nothing held, a missed change, or one V1 has no delta for: read the list again.
if (!held || !delta || baseVersion !== held.version || !apply(held.packs, delta))
return reload(spaceId);
held.version = version;
});
// false when the delta names a pack you do not hold.
function apply(packs: any[], d: any): boolean {
if (d.type === "packUpserted") {
// The pack comes without its items: keep the ones you hold.
const old = packs.find((p) => p.packId === d.pack.packId);
if (old) Object.assign(old, { ...d.pack, items: old.items });
else packs.push(d.pack);
return true;
}
if (d.type === "packsReordered") {
for (const p of packs) if (p.kind === d.kind) p.sortOrder = d.ordered.indexOf(p.packId);
return true;
}
const pack = packs.find((p) => p.packId === d.packId);
if (!pack) return false;
switch (d.type) {
case "packDeleted":
packs.splice(packs.indexOf(pack), 1);
return true;
case "itemUpserted":
pack.items = [...pack.items.filter((i: any) => i.itemId !== d.itemId), d.item];
break;
case "itemDeleted":
pack.items = pack.items.filter((i: any) => i.itemId !== d.itemId);
break;
case "itemsReordered":
for (const i of pack.items) i.sortOrder = d.ordered.indexOf(i.itemId);
break;
default:
return false; // a type added later: the list is the safe answer
}
pack.items.sort((a: any, b: any) => a.sortOrder - b.sortOrder);
return true;
} In Messages #
Bots cannot send stickers or custom emoji in messages yet. People's messages carry them as
sticker and customEmoji entities,
but V1 of the Bot API neither accepts nor delivers them. See Message Entities for what a bot sees instead.
A custom emoji can be a reaction, though: IReactions/v1/AddCustom reacts with an emoji item by its
itemId — see Reactions.
Limits & Permissions #
GetQuota has the current numbers. AddItem · IFiles/Upload, all parts together. IExpressions · IFiles, sliding window per bot. space_rate_limited past it. expressionsUpdate.