Send a premium emoji
Copy an ID from the directory, paste a snippet, and send it from your bot.
The limit
The formatting section says this, in both the HTML notes and the MarkdownV2 notes:
Custom emoji entities can only be used by bots that purchased additional usernames on Fragment or in the messages directly sent by the bot to private, group and supergroup chats if the owner of the bot has a Telegram Premium subscription.
That is a rule about who may send the entity. It is two cases:
- The bot bought additional usernames on Fragment.
- Or the bot sends the message directly to a private chat, a group, or a supergroup, and the owner of the bot has Telegram Premium.
Keyboard buttons repeat the same sentence for icon_custom_emoji_id. A supergroup can also have its own custom emoji set. The Chat field custom_emoji_sticker_set_name says custom emoji from that set can be used by all users and bots in the group. That does not apply to every ID in this directory.
The same formatting notes say what people see when the custom emoji cannot be drawn:
A valid emoji must be used as the content of the tg-emoji tag. The emoji will be shown instead of the custom emoji in places where a custom emoji cannot be displayed (e.g., system notifications) or if the message is forwarded by a non-premium user. It is recommended to use the emoji from the emoji field of the custom emoji sticker.
So the fallback character is required, and a forward by someone without Premium shows that character. The docs do not say that every recipient without Premium sees only the fallback in a direct message. If your bot is outside the two cases above, fix that before you change the snippet. A checklist is on why the emoji does not show.
Ready snippets
These use the checkmark, ID 5206607081334906820. Every emoji in the directory has the same kind of snippet. Paste one block. Do not mix it with another formatting mode.
from telegram import MessageEntity
from telegram.constants import MessageEntityType
# python-telegram-bot v20 or newer. Leave parse_mode unset.
text = "Hi ✔️"
await bot.send_message(
chat_id=CHAT_ID,
text=text,
entities=[
MessageEntity(
type=MessageEntityType.CUSTOM_EMOJI,
offset=3,
length=2,
custom_emoji_id="5206607081334906820",
)
],
)Paste this into a Python bot.
from aiogram.enums import MessageEntityType
from aiogram.types import MessageEntity
# aiogram 3. Do not also set parse_mode.
text = "Hi ✔️"
await bot.send_message(
chat_id=CHAT_ID,
text=text,
entities=[
MessageEntity(
type=MessageEntityType.CUSTOM_EMOJI,
offset=3,
length=2,
custom_emoji_id="5206607081334906820",
)
],
)Paste this into an aiogram bot.
// Telegraf 4. Keep the ID in quotes.
const text = "Hi ✔️";
await ctx.reply(text, {
entities: [
{
type: "custom_emoji",
offset: 3,
length: 2,
custom_emoji_id: "5206607081334906820",
},
],
});Paste this into a Telegraf bot.
{
"chat_id": "CHAT_ID",
"text": "Hi ✔️",
"entities": [
{
"type": "custom_emoji",
"offset": 3,
"length": 2,
"custom_emoji_id": "5206607081334906820"
}
]
}The body of a sendMessage call.
Hi <tg-emoji emoji-id="5206607081334906820">✔️</tg-emoji>Send this text with parse_mode set to HTML. Do not pass entities as well.
Hi Send this text with parse_mode set to MarkdownV2. The docs ask for a real emoji as the alternative text.
Get IDs from any pack
getStickerSet takes name, the set name. For a link https://t.me/addemoji/RestrictedEmoji the name is RestrictedEmoji. The reply is a StickerSet. On each sticker, custom_emoji_id is set for custom emoji and absent for a normal sticker. The method description does not require Telegram Premium. A normal bot token from BotFather is enough to read the pack.
getCustomEmojiStickers goes the other way. You pass custom_emoji_ids, at most 200 identifiers, as a JSON list of strings. Use it to check an ID, not to download a whole large pack in one call.
The script below is the file at /tools/fetch_pack.py. It uses only the Python standard library. It does not log your token.
#!/usr/bin/env python3
"""Dump all custom_emoji_ids of a Telegram custom emoji pack.
Usage:
export BOT_TOKEN=123456:ABC... (a normal bot token from @BotFather)
python3 fetch_pack.py RestrictedEmoji
python3 fetch_pack.py FestiveFlags RoundFlags --out flags_packs.json
python3 fetch_pack.py --env MY_TOKEN_VAR SomePack
The pack name is the last part of the link t.me/addemoji/<name>.
No Telegram Premium is needed to read a pack with a bot token.
Only the Python standard library is used.
"""
import argparse
import json
import os
import sys
import urllib.error
import urllib.parse
import urllib.request
def call(token, method, params):
url = "https://api.telegram.org/bot%s/%s" % (token, method)
data = urllib.parse.urlencode(params).encode()
try:
with urllib.request.urlopen(urllib.request.Request(url, data=data), timeout=30) as r:
return json.load(r)
except urllib.error.HTTPError as e:
try:
return json.load(e)
except Exception:
return {"ok": False, "description": "HTTP %s" % e.code}
def fetch_pack(token, name):
res = call(token, "getStickerSet", {"name": name})
if not res.get("ok"):
raise RuntimeError("%s: %s" % (name, res.get("description")))
s = res["result"]
items = []
for st in s.get("stickers", []):
cid = st.get("custom_emoji_id")
if not cid:
continue # normal sticker, not a custom emoji
items.append({
"id": cid,
"emoji": st.get("emoji", ""),
"pack": s.get("name", name),
"animated": bool(st.get("is_animated") or st.get("is_video")),
})
return s.get("title", name), s.get("sticker_type"), items
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("packs", nargs="+", help="pack name(s), from t.me/addemoji/<name>")
ap.add_argument("--env", default="BOT_TOKEN", help="env var that holds the bot token (default BOT_TOKEN)")
ap.add_argument("--out", help="write JSON to this file (default: print to screen)")
a = ap.parse_args()
token = os.environ.get(a.env)
if not token:
sys.exit("Set the %s environment variable to your bot token." % a.env)
all_items = []
for name in a.packs:
try:
title, kind, items = fetch_pack(token, name)
except RuntimeError as e:
print("skip", e, file=sys.stderr)
continue
print("%s (%s, %s): %d custom emoji" % (name, title, kind, len(items)), file=sys.stderr)
all_items.extend(items)
text = json.dumps(all_items, ensure_ascii=False, indent=1)
if a.out:
with open(a.out, "w", encoding="utf8") as f:
f.write(text)
print("wrote", a.out, file=sys.stderr)
else:
print(text)
if __name__ == "__main__":
main()Save it and run it with a bot token in the environment. The token is not written into the output.
Example, after you export the token:
export BOT_TOKEN=123456:replace-with-your-token
python3 fetch_pack.py RestrictedEmoji
python3 fetch_pack.py FestiveFlags RoundFlags --out flags_packs.jsonThe pack name is the last part of t.me/addemoji/Name. A bad name is printed and skipped.
Packs that this directory does not include, such as FestiveFlags or RoundFlags, are listed as names only in the source notes. Their IDs are not invented here. Dump them with the script if you need them.
More answers are on the emoji FAQ. A Python example is in this post.
For developers
MessageEntity calls custom_emoji_id a string. These IDs are 19 digits, all larger than 9007199254740991. The checkmark ID 5206607081334906820 becomes 5206607081334907000 if JavaScript parses it as a number.
offset and length are UTF-16 code units. JavaScript string.length already counts that way. Python len() does not. Nigeria (5411568100430587798) has UTF-16 length 4. The checkmark fallback has UTF-16 length 2. Snippets send "Hi " plus the emoji, so the offset is 3.
def utf16_len(text: str) -> int:
return len(text.encode("utf-16-le")) // 2
# Nigeria flag: Python len() is 2, UTF-16 length is 4.
# Check mark with variation selector: UTF-16 length is 2.
print(utf16_len("🇳🇬"))
print(utf16_len("✔️"))
print(utf16_len("Hi "))Use this when you build an entity yourself.
getStickerSet reads a pack by its short name. getCustomEmojiStickers looks up IDs you already have, at most 200 at a time. Categories follow the unicode group of the fallback character. They are not Telegram pack names.