Events
Events are the foundation of analytics in Metriox. Every user action in your Telegram bot can be recorded as an event.
What is an event?
An event is any action or interaction between a user and your bot:
- A button press
- A command being sent
- A screen view
- A completed purchase
- Any custom action
Event structure
Events in Metriox are platform-agnostic and consist of:

Core fields
| Field | Type | Description |
|---|---|---|
event_id | UUID | Unique identifier of the event |
event_name | String | Event name (for example, message) |
platform_user_id | String | User ID in Telegram |
session_id | UUID | User session ID |
tenant_id | UUID | ID of your bot in Metriox |
received_at | DateTime | Time the server received the event |
created_at | DateTime | Time the event was created |
Additional fields
body(String, nullable) — the text content of the eventprops(JSON) — arbitrary event properties in JSON format
One format, whatever the source
Data can reach Metriox in several ways — through a bot token (polling Telegram over MTProto), through a server-side SDK on top of the Bot API, through the WebApp SDK in the browser, or directly over the HTTP API. Whatever the source, Metriox normalizes every event into a single canonical structure on the server: the same event names, the same $ property sections, and the same field values.
That means data of any origin is equally visible and usable in the dashboard, in filters, and in segments — you never need to know how an event was captured in order to report on it.
Normalization happens once, at ingest. The fewer transformations, the more predictable the data, so the SDKs and the worker send fields as close to their final shape as possible and the server only finishes standardizing them. This does not break historical data: older events are read back in the same canonical shape.
Canonical event names (Telegram)
Telegram events get a canonical name based on the meaning of the action, not on how it was delivered. The key principle: one action, one name — the details are expressed as properties.
For example, any message — incoming or outgoing, plain text or with an attachment, new or edited — is a single message event. You tell them apart by their properties:
| Property | What it tells you |
|---|---|
$tg.direction | outbound — a bot reply, inbound — a user message |
$tg.is_edited | the message was edited |
$tg.message_type | content type (text, photo, …) |
$tg.chat_type | chat type (private, group, supergroup, channel) |
$tg.entities | text formatting (bold, links, code) |
The main canonical names:
| Event name | When it happens |
|---|---|
message | any message (see the properties above) |
callback_query | an inline button was pressed |
inline_query | an inline query to the bot |
chosen_inline_result | an inline result was chosen |
poll / poll_vote | a poll / a vote in a poll |
reaction | a reaction to a message |
pre_checkout_query, shipping_query, successful_payment | payment stages |
my_chat_member, chat_member, chat_join_request | a change in chat membership (including the bot being blocked) |
message_deleted | messages were deleted |
business_message | a message sent through Telegram Business |
Metriox uses $tg.direction to render a conversation as two sides: user messages on the left, bot replies on the right.
The field is filled in on every source, but you still have to be able to see the bot's replies. By design, the Bot API never delivers a bot's own outgoing messages back to it, so on the Bot API the bot reports them itself: wrap your client with WithMetrioxCapture(...) in the C# SDK and this happens automatically. A bot-token connection (MTProto) sees outgoing messages on its own, but it does not receive direct messages — only the SDK brings those in. See SDK for details.
Event identity and deduplication
event_id is computed deterministically from Telegram's "natural" coordinates — for example, from the chat ID and the message ID. That has two practical consequences:
- redelivery does not double your data. If Telegram or your bot sends the same update twice (a restart, a webhook retry, a reconnect), it stays a single event;
- running both connections does not double your bill. If the same bot is connected both by token and through the SDK, both paths compute the same
event_idfor one action.
This is easy to get wrong, so precisely:
| Result | |
|---|---|
event_id of the two copies | identical |
| Rows stored | two — they do not collapse into one |
| Charged against your event quota | once |
The rows stay separate on purpose: they carry different $source.channel values, and their session_id and platform_user_id may differ too, so a report pinned to one source still finds its data. The second copy is marked with $meta.unbilled_duplicate and consumes no quota.
Charge deduplication does not apply to every event type — only to those where both connections report the same natural Telegram coordinates: ordinary messages, edits (when Telegram supplied an edit time), button presses, inline queries, payment queries, and business-account messages.
Still counted separately: reactions, poll votes and chosen inline results. For every other type the question does not arise — only one connection can physically see them, so no duplicate exists.
update_idupdate_id from the Bot API may look like the obvious choice. It is not: update_id exists only in the Bot API getUpdates/webhook stream, and MTProto has no such thing at all — there, updates are numbered by session cursors (pts/qts) that differ for every client and are not a property of the event itself. That is why the identifier is built from coordinates that both connection methods can see.
Your custom events use any name you choose (purchase_completed, screen_view, …) and are marked with $event.origin = custom.
Event properties
Properties are extra information about an event that lets you analyze user behavior in more detail.
There are two kinds of properties: system properties (captured automatically by the platform, prefixed with $) and custom properties (your own arbitrary fields). See Properties for details.
Property examples
{
"button_name": "buy_subscription",
"plan": "premium",
"price": 299,
"currency": "RUB",
"screen_name": "pricing"
}
Property types
Metriox supports the following terminal data types:
- String — text values
- Number — numeric values (integer, float)
- DateTime — date and time
- Boolean — true/false
Use clear names for custom events and properties. For example, prefer subscription_button_click over btn_1_click.
Event examples
A simple custom event
{
"event_name": "bot_started",
"platform_user_id": "123456789",
"session_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
A custom event with properties
{
"event_name": "purchase_completed",
"platform_user_id": "123456789",
"props": {
"product_id": "premium_month",
"amount": 299,
"currency": "RUB",
"payment_method": "telegram_stars"
}
}
A Telegram message captured automatically
This is what the /start premium command from a user looks like after normalization. The event name is message, and the Telegram details live in the read-only system sections $event and $tg:
{
"event_name": "message",
"platform_user_id": "123456789",
"body": "/start premium",
"props": {
"$event": { "origin": "telegram", "type": "message" },
"$tg": {
"is_outgoing": false,
"chat_type": "private",
"command": "/start",
"command_params": "premium"
}
}
}
Sending events
You can send events in several ways:
- SDK — use the official SDKs for JavaScript/TypeScript or C#
- API — send events through the REST API
- Automatically — Telegram events are captured automatically once you connect a bot token
Best practices
Recommended
- Use snake_case for custom event names:
button_click,purchase_completed - Add contextual properties:
screen_name,source,category - Group similar events with properties rather than with dozens of unique names
Not recommended
- Names that are too generic:
click,action,event - Sending personal data in plain form
- Too many unique events (properties are the better tool)
What's next?
- Properties — system and custom properties
- Data types — more on the supported data types
- Dashboard — how to visualize events
- Filters — how to filter events for analysis