The low-level API provides direct access to all Telegram Bot API methods without the DSL framework. This is useful for:
- Complex applications with background tasks or scheduled jobs
- Fine-grained control over requests and responses
- Library usage without the bot framework
- Direct integration with other systems
How It Works
ExGram's low-level API is automatically generated from an up-to-date JSON description of the Telegram Bot API using the telegram_api_json project.
The Generation Process
- The
telegram_api_jsonproject scrapes the Telegram Bot API documentation - It produces a standardized JSON file with all methods, parameters, and models
- A Python script (
extractor.py) reads this JSON and generates Elixir code inlib/ex_gram.ex - As a result,
ExGramhave all the available methods andExGram.Model.*all the models, both with proper typespecs and documentation
Result: Every method has correct type specs and documentation, making the API a pleasure to use!
Models
All models are in the ExGram.Model module and match the Telegram Bot API documentation one-to-one.
Using Models
alias ExGram.Model.{User, Message, Chat, Update}
# Models have correct typespecs
@spec get_user_name(User.t()) :: String.t()
def get_user_name(%User{first_name: first, last_name: last}) do
"#{first} #{last || ""}"
endInspecting Model Types
In IEx, you can view model types:
iex> t ExGram.Model.User
@type t() :: %ExGram.Model.User{
first_name: String.t(),
id: integer(),
is_bot: boolean(),
language_code: String.t(),
last_name: String.t(),
username: String.t()
}Methods
All methods are in the ExGram module and follow these conventions:
Naming Convention
Telegram's camelCase to Elixir's snake_case
sendMessagetoExGram.send_message/3getUpdatestoExGram.get_updates/1answerCallbackQuerytoExGram.answer_callback_query/2
Method Signatures
Mandatory parameters → Function arguments (in order from docs) Optional parameters → Keyword list in last argument
# sendMessage has 2 mandatory params: chat_id, text
ExGram.send_message(chat_id, text)
ExGram.send_message(chat_id, text, parse_mode: "Markdown")
# getUpdates has 0 mandatory params, 4 optional
ExGram.get_updates()
ExGram.get_updates(offset: 123, limit: 100)Return Values
Methods return {:ok, result} | {:error, ExGram.Error.t()}:
case ExGram.send_message(chat_id, "Hello!") do
{:ok, %ExGram.Model.Message{} = message} ->
IO.puts("Message sent! ID: #{message.message_id}")
{:error, %ExGram.Error{reason: reason}} ->
Logger.error("Failed to send message: #{inspect(reason)}")
endBang Methods (!)
Every method has a ! variant that returns the result directly or raises:
# Safe version
{:ok, message} = ExGram.send_message(chat_id, "Hello")
# Bang version - returns result or raises
message = ExGram.send_message!(chat_id, "Hello")Use bang methods when you're confident the operation will succeed or when you want to let it fail.
Method Documentation
View method documentation in IEx with h:
iex> h ExGram.send_message
def send_message(chat_id, text, ops \\ [])
@spec send_message(
chat_id :: integer() | String.t(),
text :: String.t(),
ops :: [
parse_mode: String.t(),
entities: [ExGram.Model.MessageEntity.t()],
disable_web_page_preview: boolean(),
disable_notification: boolean(),
protect_content: boolean(),
reply_to_message_id: integer(),
allow_sending_without_reply: boolean(),
reply_markup:
ExGram.Model.InlineKeyboardMarkup.t()
| ExGram.Model.ReplyKeyboardMarkup.t()
| ExGram.Model.ReplyKeyboardRemove.t()
| ExGram.Model.ForceReply.t()
]
) :: {:ok, ExGram.Model.Message.t()} | {:error, ExGram.Error.t()}Extra Options
All methods support three extra options:
bot - Use named bot
ExGram.send_message(chat_id, "Hello", bot: :my_bot)The bot name has to match to a defined AND started bot, the name is the one you write in use ExGram.Bot, name: :my_bot, and you can always get the name of a bot from the module with MyBot.name/0
Note: Only use one of token or bot, not both.
token - Use specific token
ExGram.send_message("@channel", "Update!", token: "BOT_TOKEN")debug - Print HTTP response
ExGram.get_me(debug: true)
# 16:37:49.397 [info] Path: "/bot<token>/getMe"
body: %{}Warning
Do not use this in production, it will log your bot's tokens
Common Use Cases
Sending Messages from Background Tasks
defmodule MyApp.Scheduler do
def send_daily_report do
users = MyApp.Users.get_subscribed_users()
Enum.each(users, fn user ->
message = generate_report(user)
ExGram.send_message(user.telegram_id, message, bot: :my_bot)
end)
end
endSending to Channels
# No bot token in config
ExGram.send_message("@my_channel", "Update!", token: System.get_env("BOT_TOKEN"))Working with not supported on the DSL
# By file ID
{:ok, message} = ExGram.send_photo(chat_id, "BQACAgIAAxkBAAI...")
# By local path
{:ok, message} = ExGram.send_photo(chat_id, {:file, "priv/image.png"})
# By content
image_stream = generate_image_stream()
{:ok, message} = ExGram.send_photo(chat_id, {:file_content, image_stream, "image.png"})Pinning Messages
{:ok, %{message_id: msg_id}} = ExGram.send_message(chat_id, "Important!")
ExGram.pin_chat_message(chat_id, msg_id)Getting Bot Info
{:ok, %ExGram.Model.User{username: username}} = ExGram.get_me(bot: :my_bot)
IO.puts("Bot username: @#{username}")Using Without the Framework
You can use ExGram purely as a library without the bot framework:
# config/config.exs
config :ex_gram,
token: "YOUR_BOT_TOKEN",
adapter: ExGram.Adapter.Req
# lib/my_app/application.ex
# No need to add ExGram to supervision tree
# lib/my_app.ex
defmodule MyApp do
def notify_users(message) do
users = get_users()
Enum.each(users, fn user ->
ExGram.send_message(user.telegram_id, message)
end)
end
endOr without any config:
ExGram.send_message("@channel", "Update!", token: "BOT_TOKEN")Complete Example: Notification System
defmodule MyApp.Notifications do
# You can still use the ExGram.Dsl.* if you want, they are independent
import ExGram.Dsl.Keyboard
alias ExGram.Dsl.MessageEntityBuilder, as: B
alias ExGram.Model.{InlineKeyboardMarkup, InlineKeyboardButton}
def send_notification(user_id, type, data) do
{message, entities} = format_message(type, data)
keyboard = build_keyboard(type, data)
ExGram.send_message(user_id, message, reply_markup: keyboard, entities: entities, bot: :my_bot)
end
defp format_message(:order_shipped, %{order_id: id, tracking: tracking}) do
header = B.join(["📦", B.bold("Order Shipped!")])
order = B.join([
B.join([B.bold("Order"), B.code("##{id}"), "has been shipped"]),
B.join([B.bold("Tracking:"), B.url(tracking)])
], "\n")
B.join([header, order], "\n\n")
end
defp build_keyboard(:order_shipped, %{tracking_url: url}) do
keyboard :inline do
row do
button "Track Package", url: url
end
end
end
endNext Steps
- Multiple Bots - Using
bot:option effectively - Testing - Test adapter for low-level API calls