{"id":2962,"date":"2026-09-14T11:45:02","date_gmt":"2026-09-14T06:15:02","guid":{"rendered":"https:\/\/www.smsgatewaycenter.com\/blog\/?p=2962"},"modified":"2026-09-14T11:45:05","modified_gmt":"2026-09-14T06:15:05","slug":"telegram-messaging-api-chat-id-model","status":"publish","type":"post","link":"https:\/\/www.smsgatewaycenter.com\/blog\/telegram-messaging-api-chat-id-model\/","title":{"rendered":"Telegram Messaging API: Eight Endpoints, the Chat ID Model, and the Error Shape That Breaks Typed Clients"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">A field by field reference for the <strong>SMSGatewayCenter Telegram REST API<\/strong>. Covers the eight endpoints under <code>rest\/tg\/v1<\/code>, the chat ID identity model, the three names one message identifier travels under, the payload type flip on error, and a three way comparison of the Telegram, SMS and RCS delivery rows.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/telegram-messaging-api-reference-channels.webp\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"584\" src=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/telegram-messaging-api-reference-channels-1024x584.webp\" alt=\"Abstract technical diagram showing one message stream branching into three separate messaging channel columns in teal, blue and orange.\" class=\"wp-image-2963\" srcset=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/telegram-messaging-api-reference-channels-1024x584.webp 1024w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/telegram-messaging-api-reference-channels-300x171.webp 300w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/telegram-messaging-api-reference-channels-768x438.webp 768w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/telegram-messaging-api-reference-channels.webp 1200w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><figcaption class=\"wp-element-caption\">Three channels on one platform, three different wire contracts underneath.<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<h1 class=\"wp-block-heading\">TABLE OF CONTENTS<\/h1>\n\n\n\n<ol class=\"wp-block-list\">\n<li><a href=\"#the-short-answer\">The Short Answer<\/a><\/li>\n\n\n\n<li><a href=\"#tldr\">TL;DR<\/a><\/li>\n\n\n\n<li><a href=\"#one-base-path\">One Base Path, Eight Endpoints, One Bot<\/a><\/li>\n\n\n\n<li><a href=\"#identity-model\">The Identity Model: Chat IDs, Not Phone Numbers<\/a><\/li>\n\n\n\n<li><a href=\"#bot-setup\">Bot Setup, the Endpoint That Configures Infrastructure<\/a><\/li>\n\n\n\n<li><a href=\"#building-your-audience\">Building Your Audience: Recipients Is a Query, Not a Table<\/a><\/li>\n\n\n\n<li><a href=\"#sending\">Sending: One Endpoint, Eleven Message Types<\/a><\/li>\n\n\n\n<li><a href=\"#three-names\">One Identifier, Three Names, Two JSON Types<\/a><\/li>\n\n\n\n<li><a href=\"#templates\">Templates: Two Actions, Two Envelopes<\/a><\/li>\n\n\n\n<li><a href=\"#delivery-report\">The Delivery Report Field by Field<\/a><\/li>\n\n\n\n<li><a href=\"#type-flip\">The Payload Type Flip on Error<\/a><\/li>\n\n\n\n<li><a href=\"#three-way-comparison\">Telegram, SMS and RCS Side by Side<\/a><\/li>\n\n\n\n<li><a href=\"#inbox-and-webhook\">The Inbox and the Webhook That Is Not Yours<\/a><\/li>\n\n\n\n<li><a href=\"#summary-and-analytics\">Summary and Analytics: Which Number to Trust<\/a><\/li>\n\n\n\n<li><a href=\"#endpoint-reference\">Endpoint Reference Table<\/a><\/li>\n\n\n\n<li><a href=\"#first-send\">Your First Telegram Send in Six Steps<\/a><\/li>\n\n\n\n<li><a href=\"#working-code\">Working Code in Four Languages<\/a><\/li>\n\n\n\n<li><a href=\"#ten-mistakes\">Ten Mistakes That Cost a Sprint<\/a><\/li>\n\n\n\n<li><a href=\"#decision-matrix\">Choosing Between Telegram, SMS and RCS<\/a><\/li>\n\n\n\n<li><a href=\"#production-checklist\">Production Checklist<\/a><\/li>\n\n\n\n<li><a href=\"#unspecified-behaviour\">Unspecified Behaviour and How to Code Around It<\/a><\/li>\n\n\n\n<li><a href=\"#faqs\">FAQs<\/a><\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<h2 id=\"the-short-answer\" class=\"wp-block-heading\">The Short Answer<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The <a href=\"https:\/\/www.smsgatewaycenter.com\/telegram-bulk-messaging\/\"><strong>SMSGatewayCenter Telegram API<\/strong><\/a> is eight REST endpoints under a single base path, <code>https:\/\/unify.smsgateway.center\/rest\/tg\/v1\/<\/code>. You connect one bot per account with a token from Telegram BotFather, the platform registers an inbound webhook with Telegram on your behalf, and from that point you address people by Telegram <code>chatId<\/code> rather than by mobile number. Sends go to <code>\/send<\/code> as form encoded POST. Everything else is a GET: <code>\/setup<\/code>, <code>\/recipients<\/code>, <code>\/templates<\/code>, <code>\/inbox<\/code>, <code>\/dlr<\/code>, <code>\/summary<\/code> and <code>\/analytics<\/code>. All eight take <code>output=json<\/code> and all eight accept an <code>apikey<\/code> request header in place of the password, which is the variant to use everywhere.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The part that costs people a day is not the endpoint list. It is that every Telegram endpoint returns its main payload as an object or array on success and as an <strong>empty array<\/strong> on error. A Go, Rust, Java or C# client that binds <code>setup<\/code> to a struct will unmarshal the success case cleanly and throw on the 403. Bind the payload loosely, branch on the top level <code>status<\/code> string before you touch anything else, and the family becomes easy.<\/p>\n\n\n\n<h2 id=\"tldr\" class=\"wp-block-heading\">TL;DR<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>One base path, one bot, one auth header.<\/strong> All eight endpoints sit under <code>rest\/tg\/v1\/<\/code>, and all eight accept the <code>apikey<\/code> header. A legacy alias without the <code>v1<\/code> segment works on seven of them. The account is bound to exactly one Telegram bot, connected through <code>\/setup<\/code> with a BotFather token.<\/li>\n\n\n\n<li><strong>Chat IDs, not phone numbers.<\/strong> Telegram addresses a conversation, not a handset. <code>chatId<\/code> and <code>telegramUserId<\/code> are two different numbers for the same person and only <code>chatId<\/code> is addressable. You may send by <code>phoneNumber<\/code> instead, but only for contacts who shared their number with your bot.<\/li>\n\n\n\n<li><strong>Your audience is opt in and it is a query.<\/strong> <code>\/recipients<\/code> returns distinct inbox contacts for a <strong>date range<\/strong>. It is not a durable subscriber table. Maintain your own.<\/li>\n\n\n\n<li><strong>One identifier, three names.<\/strong> The send response publishes <code>uniqueId<\/code> unquoted, and <code>uuId<\/code> and <code>transactionId<\/code> as the same quoted string. The delivery report documents only <code>uuId<\/code> and <code>transactionId<\/code> as matching what you sent. Persist <code>uuId<\/code>.<\/li>\n\n\n\n<li><strong><code>globalErrorCode<\/code> is binary here.<\/strong> <code>0<\/code> delivered, <code>1<\/code> failed. On SMS it is a rich taxonomy and on RCS it carries values such as <code>5007<\/code>. Do not share an error mapping table across channels.<\/li>\n\n\n\n<li><strong><code>isFinal<\/code> exists and nothing else on the platform has it.<\/strong> The delivery row tells you outright whether the status is terminal, so your reconciler knows when to stop polling.<\/li>\n\n\n\n<li><strong>Three timestamps, two of them formatted.<\/strong> <code>submitTime<\/code>, <code>dispatchedTime<\/code> and <code>deliveryTime<\/code> are unquoted millisecond epochs. <code>submitTime<\/code> and <code>deliveryTime<\/code> have <code>Formatted<\/code> string twins. <code>dispatchedTime<\/code> does not.<\/li>\n\n\n\n<li><strong>Cost is called <code>charges<\/code> and it is a quoted string.<\/strong> SMS calls it <code>amount<\/code> and returns an unquoted float. RCS calls it <code>amount<\/code> and returns a quoted string with four decimals. Three channels, three contracts.<\/li>\n\n\n\n<li><strong>The webhook in <code>\/setup<\/code> is inbound to the platform, not outbound to you.<\/strong> There is no documented push to your server on this channel. Reconciliation is polling.<\/li>\n\n\n\n<li><strong>Presentation fields leak into the contract.<\/strong> <code>\/analytics<\/code> returns hex colour codes. <code>\/setup<\/code> returns a <code>formMode<\/code> field. Bind what you need and ignore the rest.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"one-base-path\" class=\"wp-block-heading\">One Base Path, Eight Endpoints, One Bot<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Every messaging channel on this platform has its own base path, and the base path is the thing that tells you which conventions apply. SMS sends go to <code>SMSApi\/send<\/code>. WhatsApp goes to <code>WAApi\/send<\/code>. RCS spreads itself across two generations, <code>RCSApi\/<\/code> for sends and templates and <code>rest\/rcs\/v1\/<\/code> for reporting, with different spellings of the format parameter on each.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Telegram is the cleanest of the four. All eight endpoints live under one path:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>https:&#47;&#47;unify.smsgateway.center\/rest\/tg\/v1\/<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">and the format parameter is spelled <code>output<\/code> on all of them, matching the <code>rest\/<\/code> generation convention rather than the <code>format<\/code> spelling used by <code>RCSApi\/<\/code>. Seven of the eight also accept a legacy alias with the <code>v1<\/code> segment removed, for example <code>\/rest\/tg\/inbox<\/code>. Write the <code>v1<\/code> form. The alias exists for older integrations and gives you nothing.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The single most consequential structural fact is that the account holds <strong>one bot<\/strong>. Not one bot per brand, not one per environment. The send endpoint has no bot selector parameter at all, and the documentation states plainly that the message is sent using the Telegram bot linked to the account making the call. Compare RCS, where <code>botId<\/code> is a required send parameter and <code>rest\/rcs\/v1\/bots<\/code> lists several. On Telegram there is nothing to select because there is nothing to choose between.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That has an immediate deployment consequence. If staging and production share an account, they share a bot, and a <code>save<\/code> call from either one overwrites the token for both. Give non production its own account. This is the same shape as the single SMS webhook slot described in the <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/observability-for-messaging-pipelines\/\">delivery report ingestion article<\/a>, and it bites in the same way: the resource is account scoped, the API offers no identifier, and nothing in the parameter list warns you.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Authentication follows the platform rule that holds everywhere: <strong>a token is an authorization, never an identity.<\/strong> <code>userid<\/code> is required on every single call regardless of how you authenticate. If you send the <code>apikey<\/code> header you still send <code>userid<\/code> alongside it. The <a href=\"https:\/\/www.smsgatewaycenter.com\/developer-api\/authentication\/\">authentication documentation<\/a> covers the four mechanisms available across the platform; on Telegram the two that matter are userid plus password, and userid plus an <code>apikey<\/code> header.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The <code>apikey<\/code> header is accepted on every endpoint in the family, and on every other API on the platform.<\/strong> Some of the Telegram pages show only the userid and password cURL variant, so the header is easy to miss on <code>\/inbox<\/code>, <code>\/dlr<\/code> and <code>\/summary<\/code>, but it works there exactly as it does on <code>\/setup<\/code> and <code>\/send<\/code>. Build one authenticated client, give it the header, and use it everywhere.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The only auth detail that genuinely varies is whether <code>output<\/code> is required:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Endpoint<\/th><th><code>output<\/code> required<\/th><th>Auth<\/th><\/tr><\/thead><tbody><tr><td><code>\/setup<\/code><\/td><td>Yes<\/td><td><code>apikey<\/code> header, or password<\/td><\/tr><tr><td><code>\/send<\/code><\/td><td>Optional<\/td><td><code>apikey<\/code> header, or password<\/td><\/tr><tr><td><code>\/recipients<\/code><\/td><td>Yes<\/td><td><code>apikey<\/code> header, or password<\/td><\/tr><tr><td><code>\/templates<\/code><\/td><td>Yes<\/td><td><code>apikey<\/code> header, or password<\/td><\/tr><tr><td><code>\/inbox<\/code><\/td><td>Yes<\/td><td><code>apikey<\/code> header, or password<\/td><\/tr><tr><td><code>\/dlr<\/code><\/td><td>Yes<\/td><td><code>apikey<\/code> header, or password<\/td><\/tr><tr><td><code>\/summary<\/code><\/td><td>Yes<\/td><td><code>apikey<\/code> header, or password<\/td><\/tr><tr><td><code>\/analytics<\/code><\/td><td>Yes<\/td><td><code>apikey<\/code> header, or password<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Prefer the header in every environment. It keeps the account password out of your configuration, out of your request logs and, on the GET endpoints, out of the query string, which is the single easiest credential leak to create on this API. Send <code>output=json<\/code> on all eight; it costs nothing on <code>\/send<\/code> and removes one thing to remember.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/diagram-telegram-api-surface.svg\"><img decoding=\"async\" src=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/diagram-telegram-api-surface.svg\" alt=\"Diagram of the eight SMSGatewayCenter Telegram REST endpoints grouped by function under the shared rest\/tg\/v1 base path, with the shared authentication and error handling rules that apply to all of them.\" class=\"wp-image-2964\"\/><\/a><figcaption class=\"wp-element-caption\">Eight endpoints, one base path, one bot. The grouping is by what the endpoint does to your system, not by HTTP verb.<\/figcaption><\/figure>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"identity-model\" class=\"wp-block-heading\">The Identity Model: Chat IDs, Not Phone Numbers<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This is the conceptual jump, and everything downstream depends on getting it right.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">SMS addresses a handset. You have a mobile number, you send to it, and the recipient&#8217;s consent is a legal and regulatory matter handled outside the protocol through <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/dlt-registration-step-by-step-guide-india\/\">DLT registration<\/a> and consent capture. The number exists whether or not the person has ever heard of you.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Telegram addresses a <strong>conversation<\/strong>. A <code>chatId<\/code> comes into existence only when a person opens your bot and sends it something, usually <code>\/start<\/code>. Before that moment there is no address to send to. Consent is not a compliance overlay bolted onto an addressable identifier; consent is what creates the identifier. That is a better privacy model and a harder growth model, and both facts shape how you build.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The recipients row makes the identity structure explicit:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n    \"chatId\": \"123456789\",\n    \"telegramUserId\": \"987654321\",\n    \"displayName\": \"Rahul\",\n    \"firstName\": \"Rahul\",\n    \"username\": \"rahul_demo\"\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Note carefully: <strong><code>chatId<\/code> and <code>telegramUserId<\/code> are different values.<\/strong> They are not two spellings of one number. <code>chatId<\/code> identifies the conversation between that person and your bot and is the only one you can send to. <code>telegramUserId<\/code> identifies the Telegram account and appears as a filter on <code>\/inbox<\/code> but is not accepted by <code>\/send<\/code>. If you store one column and call it &#8220;telegram id&#8221; you will eventually put the wrong number in it, and the failure mode is a send that is accepted and goes nowhere useful. Store both, name them exactly as the API does, and make the send path read <code>chatId<\/code> and only <code>chatId<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Both come back as <strong>quoted strings<\/strong> on read even though they are numeric and even though the send endpoint accepts them as bare digits in a form field. Keep them as strings end to end. Telegram chat IDs for groups and channels are negative and can be large, and there is no upside to parsing them.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>username<\/code> is the public @handle without the leading @. It is user changeable and must never be a key. <code>displayName<\/code> and <code>firstName<\/code> carry the same value in the documented sample, and there is no <code>lastName<\/code> field on the row at all even though the contact card message type has one. Treat every name field as decorative, present it in your support UI, and key nothing on it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The third addressing route is <code>phoneNumber<\/code>, passed on <code>\/send<\/code> instead of <code>chatId<\/code>. The documentation is precise about its limits: it matches contacts who shared their phone number with your bot, which in Telegram means the person tapped a share contact button at some point. It is a convenience for bridging an existing CRM keyed on mobile numbers, not a way to reach arbitrary handsets. If you are trying to reach someone who has never interacted with your bot, Telegram is not the channel; that send belongs on SMS, and <a href=\"https:\/\/www.smsgatewaycenter.com\/multi-channel-fallback\/\">multi channel fallback<\/a> is the mechanism for expressing that preference once rather than branching on it in every call site.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">One request rule, stated as a validation rule on the send page and worth honouring in your own code before the API ever sees it: use either <code>chatId<\/code> or <code>phoneNumber<\/code>, never both in the same request.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"bot-setup\" class=\"wp-block-heading\">Bot Setup, the Endpoint That Configures Infrastructure<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><code>\/setup<\/code> has no analogue anywhere else on the platform. SMS sender IDs and RCS bots are provisioned through approval workflows with their own CRUD families. Telegram gives you a single endpoint that creates the identity, stores the credential and wires the inbound plumbing in one call.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">It takes three actions, and unusually for this platform each has documented aliases:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Action<\/th><th>Method<\/th><th>Aliases<\/th><th>What it does<\/th><\/tr><\/thead><tbody><tr><td><code>get<\/code><\/td><td>GET<\/td><td>empty, <code>list<\/code>, <code>status<\/code><\/td><td>Returns connection status, feature flags and <code>botChatUrl<\/code><\/td><\/tr><tr><td><code>save<\/code><\/td><td>POST<\/td><td><code>connect<\/code>, <code>update<\/code><\/td><td>Creates or updates the bot, then registers the inbound webhook<\/td><\/tr><tr><td><code>registerwebhook<\/code><\/td><td>POST<\/td><td><code>register_webhook<\/code>, <code>webhook<\/code><\/td><td>Re-registers the inbound webhook only<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Write the canonical spelling in your code. The aliases are there for compatibility and every extra accepted spelling is a way for two services in your estate to drift apart while both appear to work.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The parameters are few. <code>botName<\/code> is a display name of two to one hundred characters, required on save. <code>botHandle<\/code> is the bot username without the @, four to thirty two characters of letters, numbers and underscore, used to build <code>https:\/\/t.me\/{handle}<\/code>. <code>botToken<\/code> is the BotFather token in <code>digits:token<\/code> form and is described as create only: optional on update, where omitting it keeps the existing token.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That last detail deserves attention because it is a security property, not a convenience. <strong>There is no way to read the bot token back out.<\/strong> Contrast <code>apikey\/read<\/code>, which returns your API key in plaintext and therefore stores it recoverably. The Telegram token is write only through this API. Treat that as the intended design and match it in your own systems: put the token in a secret manager, pass it once at connect time, and never log the request body of a <code>save<\/code> call.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The success payload is the most information dense object in the family:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n    \"status\": \"success\",\n    \"setup\": {\n        \"isConfigured\": true,\n        \"isActive\": true,\n        \"formMode\": \"update\",\n        \"botName\": \"My Store Support\",\n        \"botHandle\": \"mystorebot\",\n        \"botChatUrl\": \"https:\/\/t.me\/mystorebot\",\n        \"hasBotToken\": true,\n        \"tps\": 10,\n        \"canRegisterWebhook\": true,\n        \"uiSendingEnabled\": true,\n        \"apiAccessEnabled\": true,\n        \"maxAgents\": 5,\n        \"saved\": true,\n        \"webhook\": {\n            \"status\": \"success\",\n            \"message\": \"Webhook registered successfully.\"\n        }\n    },\n    \"statusCode\": \"200\",\n    \"reason\": \"Your Telegram bot has been connected and the webhook registered.\"\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Several things here are worth naming.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>These are real JSON booleans.<\/strong> <code>isConfigured<\/code>, <code>isActive<\/code>, <code>hasBotToken<\/code>, <code>canRegisterWebhook<\/code>, <code>uiSendingEnabled<\/code>, <code>apiAccessEnabled<\/code> and <code>saved<\/code> are <code>true<\/code>, not <code>\"1\"<\/code> and not <code>1<\/code>. Almost nothing else on this platform does that. Group and contact rows use quoted <code>\"1\"<\/code>. RCS bot and template rows use unquoted integer <code>1<\/code>. The SMS delivery row has exactly one genuine boolean, <code>flashMsg<\/code>. If you have a shared helper that coerces platform truthiness from strings and integers, it will work here by accident, but write the Telegram binding against real booleans and do not let the helper hide the difference.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>tps<\/code> is a number you can read.<\/strong> It is an unquoted integer and it is the rate at which your account may send. This is the first per account throughput figure the platform exposes programmatically anywhere, and it is worth wiring into your sender rather than hardcoding a guess. Read it at startup, use it to size your token bucket, and re-read it after any plan change. The general approach to shaping a sender around a published rate is covered in <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/rate-limiting-backpressure-messaging-systems\/\">rate limiting and backpressure in messaging systems<\/a>; the difference on Telegram is that you no longer have to infer the number.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>apiAccessEnabled<\/code> and <code>uiSendingEnabled<\/code> are separate flags.<\/strong> Your account can send from the panel while the API is closed, which is exactly the state that produces the send error <code>\"Telegram API sending is disabled for this account.\"<\/code> Check <code>apiAccessEnabled<\/code> in your deployment smoke test and fail the deploy loudly rather than discovering it on the first real broadcast.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>webhook<\/code> is a nested envelope with its own <code>status<\/code>.<\/strong> And its text field is called <code>message<\/code>, while the outer text field is called <code>reason<\/code>. Two levels, two vocabularies. A parser that searches the whole document for a key called <code>status<\/code> will find two and pick whichever comes first.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>formMode<\/code> is a user interface concept.<\/strong> It tells you whether the panel would render a create form or an edit form. It has no business being in an API response and it is a clear sign of what these endpoints are: the panel&#8217;s own backend, promoted to a public contract. That is not a criticism, it is a fact you can use. Bind the fields you actually need, ignore the rest, and never assert on a field you cannot explain the purpose of.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>botChatUrl<\/code> field exists so your own apps can render a QR code client side from <code>https:\/\/t.me\/{botHandle}<\/code>. There is no server side QR image endpoint, which is stated outright on the page. Generate it locally with any QR library; the URL is the whole payload.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"building-your-audience\" class=\"wp-block-heading\">Building Your Audience: Recipients Is a Query, Not a Table<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Read the description of <code>\/recipients<\/code> slowly, because the wording carries the whole architecture: it fetches <strong>distinct Telegram inbox contacts for a date range<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Your audience, as exposed by the API, is a projection over inbound messages, filtered by when those messages arrived. It is not a subscriber table with a stable primary key that you can page through from the beginning of time. A parent who subscribed to your school bot in April and has not messaged since will not appear in a query for the last seven days.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The defensive shape is straightforward and you should build it on day one, before you have enough subscribers for the problem to hurt.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Maintain your own <code>telegram_subscriber<\/code> table keyed on <code>chat_id<\/code>. Run a sweep job that walks <code>\/recipients<\/code> from the date your bot went live up to today in windows of a week or two, pages through each window at <code>limit=200<\/code>, and upserts every row it sees. Record <code>first_seen_at<\/code> on insert and <code>last_seen_at<\/code> on every touch. After the initial backfill, a nightly sweep over the last few days keeps it current for the cost of a handful of requests. Your broadcast code then reads your table, never the API.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This gives you three things the API alone cannot: a stable audience that does not shrink when someone goes quiet, a place to record suppression and preference state, and a join key for the rest of your data model. The schema considerations are the same ones covered in <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/outbound-message-table-schema-design\/\">the outbound message table<\/a>, and the subscriber table is its natural sibling.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The paging contract is the <code>rest\/<\/code> generation standard: <code>page<\/code> starting at 1, <code>limit<\/code> defaulting to 50 with a maximum of 200. Offset paging, not cursors. That differs from the SMS delivery report, which uses an opaque <code>cursors.next<\/code> token fed back as <code>startCursor<\/code>. Do not share a pagination helper between the two.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The date parameters accept <code>YYYY-MM-DD<\/code> or a full date and time, and carry the aliases <code>from<\/code> and <code>to<\/code>. The envelope echoes what it actually used, expanded:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\"fromDate\": \"2026-07-14 00:00:00\",\n\"toDate\": \"2026-07-14 23:59:59\",<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">That echo is genuinely useful. A date only input is expanded to the full day, inclusive at both ends, and the response tells you so. Log the echoed values rather than the values you sent, and your window arithmetic becomes auditable. No timezone is stated anywhere in the family, which is the standard situation on this platform, and the way to stay correct regardless is in the unspecified behaviour section below.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"sending\" class=\"wp-block-heading\">Sending: One Endpoint, Eleven Message Types<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><code>POST https:\/\/unify.smsgateway.center\/rest\/tg\/v1\/send<\/code>, content type <code>application\/x-www-form-urlencoded<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>msgType<\/code> selects among eleven values: <code>text<\/code>, <code>photo<\/code>, <code>audio<\/code>, <code>document<\/code>, <code>video<\/code>, <code>animation<\/code>, <code>voice<\/code>, <code>video_note<\/code>, <code>location<\/code>, <code>contact<\/code> and <code>poll<\/code>. This is a far wider surface than any other channel here. SMS offers <code>text<\/code> and <code>unicode<\/code>. RCS offers <code>text<\/code> with the richness pushed into the template. Telegram lets you send a poll.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The parameter set is small because the complexity is pushed into two JSON string fields:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Parameter<\/th><th>Required<\/th><th>Notes<\/th><\/tr><\/thead><tbody><tr><td><code>userid<\/code><\/td><td>Yes<\/td><td>Required even with the <code>apikey<\/code> header<\/td><\/tr><tr><td><code>password<\/code><\/td><td>Conditional<\/td><td>Required when <code>apikey<\/code> is not sent<\/td><\/tr><tr><td><code>msgType<\/code><\/td><td>Yes<\/td><td>One of the eleven values above<\/td><\/tr><tr><td><code>chatId<\/code><\/td><td>Conditional<\/td><td>Comma separated for multiple recipients. Not with <code>phoneNumber<\/code><\/td><\/tr><tr><td><code>phoneNumber<\/code><\/td><td>Conditional<\/td><td>Country code, digits only. Not with <code>chatId<\/code><\/td><\/tr><tr><td><code>text<\/code><\/td><td>Conditional<\/td><td>Body for <code>text<\/code>, caption for media<\/td><\/tr><tr><td><code>mediaPayload<\/code><\/td><td>Conditional<\/td><td>JSON string, extra fields for non text types<\/td><\/tr><tr><td><code>replyMarkup<\/code><\/td><td>Optional<\/td><td>JSON string in Telegram Bot API <code>reply_markup<\/code> format<\/td><\/tr><tr><td><code>output<\/code><\/td><td>Optional<\/td><td>Use <code>json<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The length limit changes with <code>msgType<\/code> and nothing warns you.<\/strong> A <code>text<\/code> message allows 4096 characters. A caption on a media message allows 1024. If your code composes a body and then decides whether to attach an image, you have a path where adding a picture silently invalidates a message that was fine a moment ago. Validate against the limit that applies to the type you are about to send, not against a constant. This is the Telegram equivalent of the GSM 03.38 segment counting trap that shows up on SMS, discussed in <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/sms-length-calculation-credits-counted\/\">SMS length calculation and how credits are counted<\/a>, except that here the failure is a rejection rather than a surprise bill.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>mediaPayload<\/code> is a JSON string, not a JSON object. You serialise it and put the resulting string in a form field. Its documented fields:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Field<\/th><th>Used for<\/th><th>Notes<\/th><\/tr><\/thead><tbody><tr><td><code>mediaUrl<\/code><\/td><td>photo, audio, document, video, animation, voice, video_note<\/td><td>Public <code>https:\/\/<\/code> URL<\/td><\/tr><tr><td><code>latitude<\/code>, <code>longitude<\/code><\/td><td>location<\/td><td>Latitude -90 to 90, longitude -180 to 180<\/td><\/tr><tr><td><code>phoneNumber<\/code>, <code>firstName<\/code>, <code>lastName<\/code><\/td><td>contact<\/td><td>Phone may include a leading <code>+<\/code><\/td><\/tr><tr><td><code>pollQuestion<\/code>, <code>pollOptions<\/code>, <code>isAnonymous<\/code><\/td><td>poll<\/td><td>Question max 300 characters, 2 to 10 options as a JSON array, <code>isAnonymous<\/code> is <code>0<\/code> or <code>1<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Two traps live in that table.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>phoneNumber<\/code> means two different things in one request.<\/strong> At the top level it selects the recipient. Inside <code>mediaPayload<\/code> it is the number printed on a contact card you are sending to someone else. A helper that builds request bodies from a flat dictionary will collide these. Namespace them in your own code before they meet the wire.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Phone formats are inconsistent between the two positions.<\/strong> The top level <code>phoneNumber<\/code> is documented as digits only with country code. The contact card <code>phoneNumber<\/code> may include a leading <code>+<\/code>. Normalise on the way in and format on the way out, per position.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Media must be a public HTTPS URL. The platform fetches it; you are not uploading bytes. That means your media host is now in the delivery path, and a signed URL that expires in sixty seconds may expire before the fetch happens. Use stable URLs with a long or absent expiry, and serve them from somewhere with real availability.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>replyMarkup<\/code> is passed through to Telegram in the Bot API&#8217;s own <code>reply_markup<\/code> format, so inline keyboards, reply keyboards, force reply and remove keyboard all work. This is worth pausing on: it means the interactive surface of your messages is governed by <a href=\"https:\/\/core.telegram.org\/bots\/faq\" target=\"_blank\" rel=\"noopener nofollow\">Telegram&#8217;s own Bot API documentation<\/a>, not by this platform&#8217;s. When a button behaves oddly, the answer is in Telegram&#8217;s spec.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Multiple recipients go in <code>chatId<\/code> as a comma separated list, exactly as SMS puts multiple numbers in <code>mobile<\/code>. And that brings us to the response.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"three-names\" class=\"wp-block-heading\">One Identifier, Three Names, Two JSON Types<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Here is the documented success response for a send:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n    \"status\": \"success\",\n    \"message\": \"Message sent successfully.\",\n    \"uniqueId\": 17123456789012,\n    \"uuId\": \"17123456789012\",\n    \"transactionId\": \"17123456789012\",\n    \"msgId\": \"42\"\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">One value appears three times under three field names, and it appears both as an unquoted JSON number and as a quoted string in the same object.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The correlation advice follows directly from what the delivery report page states, which is that <code>uuId<\/code> and <code>transactionId<\/code> are the same value returned when you send. It does not extend that guarantee to <code>uniqueId<\/code>, and the delivery report&#8217;s own sample row shows <code>uniqueId<\/code> as <code>\"90001\"<\/code> while <code>uuId<\/code> on that same row is <code>\"1234567890123456789\"<\/code>. Two samples, two different relationships. <strong>Persist <code>uuId<\/code>, reconcile on <code>uuId<\/code>, and treat <code>uniqueId<\/code> as an internal number you happen to be able to see.<\/strong> Store it if you like, as a string, and never join on it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The unquoted <code>uniqueId<\/code> is a numeric literal in a JSON document and is therefore subject to the language you parse it in. At fourteen digits the documented sample is comfortably inside JavaScript&#8217;s safe integer range, but nineteen digit identifiers exist elsewhere on this platform, and the <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/sms-api-nodejs-integration-tutorial\/\">Node.js integration tutorial<\/a> covers what happens when one of those lands in a double. Since <code>uuId<\/code> gives you the same information as a string, there is no reason to go near the number.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>msgId<\/code> is <code>\"42\"<\/code>, a quoted small integer, and it is the Telegram message ID: the identifier Telegram itself assigned inside that chat. Note that <code>msgId<\/code> now means three unrelated things across three channels. On the SMS delivery row it is a repeating free form label that shows the same value on different transactions. On RCS it is an opaque token. On Telegram it is a genuine Telegram side message number. A shared deserialiser that maps <code>msgId<\/code> to one concept is wrong on at least two channels.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Now the two structural facts about this response.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>It carries no <code>statusCode<\/code> and no <code>reason<\/code>.<\/strong> Every other endpoint in the family returns both. <code>\/send<\/code> returns <code>message<\/code> instead. So within one product, on one base path, the success envelope of the write endpoint does not match the success envelope of the seven read endpoints. Branch on <code>status<\/code>, read the human text from <code>message<\/code> on send and from <code>reason<\/code> everywhere else, and never write a generic <code>is_ok(payload)<\/code> helper that keys on the presence of <code>statusCode<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The error response has two keys and nothing else:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n    \"status\": \"error\",\n    \"message\": \"Telegram API sending is disabled for this account.\"\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Every identifier field disappears. In a typed language, only <code>status<\/code> and <code>message<\/code> may be bound as required. Everything else is optional.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>And there is no per recipient result.<\/strong> Send to three comma separated chat IDs and you get back one identifier and one status. There is no array of outcomes, and no <code>invalidMobile<\/code> style field of the kind SMS and RCS both return. You cannot tell from the response which of the three were accepted.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">So make the decision that stays correct either way: send one recipient per call for anything you need to account for individually. The reasoning is in the unspecified behaviour section below.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/diagram-telegram-identifier-chain.svg\"><img decoding=\"async\" src=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/diagram-telegram-identifier-chain.svg\" alt=\"Diagram tracing a Telegram message identifier from the send response through to the delivery report row, showing which field names carry the same value and which do not.\" class=\"wp-image-2965\"\/><\/a><figcaption class=\"wp-element-caption\">Follow uuId. It is the one name that means the same thing at both ends.<\/figcaption><\/figure>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"templates\" class=\"wp-block-heading\">Templates: Two Actions, Two Envelopes<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><code>GET https:\/\/unify.smsgateway.center\/rest\/tg\/v1\/templates<\/code><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Telegram templates are a convenience, not a gate. Unlike DLT registered SMS content, which cannot be delivered in India without an approved template, and unlike RCS, where the template is the message, a Telegram template is a saved compose payload you may choose to reuse. Nothing stops you sending arbitrary text.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Two actions, and this endpoint is the only one in the family that offers more than <code>list<\/code>:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>action=list<\/code> returns every active template as a summary row:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n    \"templateId\": 12,\n    \"templateName\": \"Order Confirmation\",\n    \"msgType\": \"text\",\n    \"mediaUrl\": \"\"\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>action=get<\/code> with <code>templateId<\/code> returns the full compose payload:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n    \"templateId\": 12,\n    \"templateName\": \"Order Confirmation\",\n    \"msgType\": \"text\",\n    \"body\": \"Hi {name}, your order #{orderId} has shipped!\",\n    \"mediaUrl\": \"\",\n    \"mediaPayloadJson\": \"\",\n    \"buttonsJson\": \"\",\n    \"useReplyMarkup\": 0\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Four things to take from this.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The placeholder syntax is single curly braces.<\/strong> <code>{name}<\/code>, <code>{orderId}<\/code>. That is a third distinct syntax on one platform. DLT templates use <code>{#var#}<\/code>. RCS templates use square brackets, <code>[name]<\/code>. If you have a substitution routine, it is channel specific, and sharing it across channels will produce messages that deliver successfully with the placeholder still visible in the body. Worse, naive string replacement carries its own hazards in several languages; the <code>$&amp;<\/code> expansion problem documented in the Node.js tutorial applies here identically.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>useReplyMarkup<\/code> is an unquoted integer <code>0<\/code>, not a boolean.<\/strong> The setup endpoint hands you real booleans; this row does not. Same family, same response generation, different convention. Coerce explicitly.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Two fields are JSON encoded strings.<\/strong> <code>mediaPayloadJson<\/code> and <code>buttonsJson<\/code> hold serialised JSON inside a JSON string, so reading a template means a second parse. Both are empty in the documented sample, and an empty string is not valid JSON. Guard for empty before parsing or your template reader will throw on every text template.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The two actions return different envelope shapes.<\/strong> <code>list<\/code> returns <code>templatesList<\/code> as an array plus <code>totalRecords<\/code>, <code>page<\/code> and <code>limit<\/code>. <code>get<\/code> returns <code>template<\/code> as a single <strong>object<\/strong> with none of the paging fields. Same URL, same endpoint, two response shapes selected by a query parameter. Model them as two distinct operations in your client.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The error shapes differ too, and the page says so explicitly: for <code>get<\/code> validation or not found errors the payload uses an empty <code>template<\/code> array rather than <code>templatesList<\/code>, with status codes such as <code>400<\/code> or <code>404<\/code>. Which leads directly to the next section.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"delivery-report\" class=\"wp-block-heading\">The Delivery Report Field by Field<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><code>GET https:\/\/unify.smsgateway.center\/rest\/tg\/v1\/dlr<\/code><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This is the endpoint you will spend the most time with, because it is the only way to learn what happened to a message. Here is the documented row, annotated by JSON type:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n    \"logId\": \"501\",\n    \"uniqueId\": \"90001\",\n    \"uuId\": \"1234567890123456789\",\n    \"transactionId\": \"1234567890123456789\",\n    \"chatId\": \"123456789\",\n    \"phoneNumber\": \"919999999999\",\n    \"msgId\": \"42\",\n    \"message\": \"Your order is confirmed.\",\n    \"msgType\": \"text\",\n    \"charges\": \"0.50\",\n    \"sendMethod\": 1,\n    \"globalErrorCode\": 0,\n    \"deliveryStatus\": \"delivered\",\n    \"vendorErrorCode\": \"\",\n    \"channel\": 1,\n    \"channelName\": \"API\",\n    \"isFinal\": 1,\n    \"submitTime\": 1712345678901,\n    \"submitTimeFormatted\": \"2026-07-07 10:14:38\",\n    \"dispatchedTime\": 1712345680000,\n    \"deliveryTime\": 1712345681000,\n    \"deliveryTimeFormatted\": \"2026-07-07 10:14:41\"\n}<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Five identifiers on one row<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>logId<\/code>, <code>uniqueId<\/code>, <code>uuId<\/code>, <code>transactionId<\/code> and <code>msgId<\/code>. <code>uuId<\/code> and <code>transactionId<\/code> are the same value. <code>logId<\/code> is the row&#8217;s own key in the reporting store. <code>msgId<\/code> is Telegram&#8217;s message number. <code>uniqueId<\/code> is an internal number and, as established above, is not the same value you saw on the send response. Key your reconciliation on <code>uuId<\/code>, store <code>logId<\/code> for support conversations, and store <code>msgId<\/code> if you ever need to correlate with something a user forwarded you from inside Telegram.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><code>isFinal<\/code> is the field no other channel has<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>isFinal: 1<\/code> tells you the status will not change again. On SMS and on RCS you must infer terminality from the status vocabulary, which means hardcoding a list of terminal status strings and hoping a new one never appears. Telegram answers the question directly.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Use it. Your reconciler should stop polling a row the moment <code>isFinal<\/code> comes back truthy, and should keep polling with backoff while it is falsy. That removes the most common cause of the stuck row problem, and it removes the need to guess how long to wait before giving up. Combined with the <code>pending<\/code> counter on the summary endpoint you get two independent readings of the same question, which is the sort of redundancy worth having in a reconciler.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><code>globalErrorCode<\/code> is binary and that is a real difference<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>0<\/code> is delivered, <code>1<\/code> is failed. That is the entire code space, and <code>deliveryStatus<\/code> is described as a readable label derived from it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">On SMS, <code>globalErrorCode<\/code> is a rich taxonomy: <code>17<\/code> is a template mismatch, and the whole catalogue is queryable from the <a href=\"https:\/\/www.smsgatewaycenter.com\/developer-api\/get-delivery-error-code-list\/\">delivery error code list endpoint<\/a>. On RCS it carries codes such as <code>5007<\/code>. On Telegram it is a flag.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">So a shared error mapping table across channels will either overmatch, treating Telegram&#8217;s <code>1<\/code> as whatever <code>1<\/code> means on SMS, or silently fail to explain any Telegram failure at all. Key your mapping on channel plus code, never on code alone. And note that the granular reason, when there is one, is meant to live in <code>vendorErrorCode<\/code>, which is an empty string on the documented successful row. Capture it, do not branch on it until you have seen real values.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>deliveryStatus<\/code> is lowercase here: <code>\"delivered\"<\/code>, <code>\"failed\"<\/code>. On RCS and SMS the equivalent values are uppercase. Compare case insensitively or normalise on ingest.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The field names are swapped relative to SMS, again<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">On the Telegram and RCS delivery rows, the outcome field is <code>deliveryStatus<\/code>. On the SMS delivery row, the outcome field is <code>status<\/code>, and on Telegram <code>status<\/code> at envelope level means whether the API call itself worked. This is the same swap documented for RCS, and it now holds across two of the three channels, which makes it the convention rather than the exception. Porting SMS reporting code to either newer channel means renaming this field, and the failure if you do not is quiet: your code reads <code>status<\/code>, finds <code>\"success\"<\/code>, and records every message as delivered.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Three timestamps, one of them without a twin<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>submitTime<\/code>, <code>dispatchedTime<\/code> and <code>deliveryTime<\/code> are unquoted millisecond epochs. <code>submitTime<\/code> and <code>deliveryTime<\/code> each have a <code>Formatted<\/code> string companion. <strong><code>dispatchedTime<\/code> does not.<\/strong> If your ingest maps epoch and formatted fields in pairs, this one will fall out of the pattern.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The three together are genuinely useful, more so than the two you get on SMS. <code>submitTime<\/code> is when you handed the message over. <code>dispatchedTime<\/code> is when it left toward Telegram. <code>deliveryTime<\/code> is when it landed. The gap between the first two is your platform queue depth; the gap between the last two is Telegram&#8217;s. When a broadcast feels slow, that split tells you which side to look at, and it is the kind of measurement that turns <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/observability-for-messaging-pipelines\/\">observability for messaging pipelines<\/a> from a dashboard into a diagnosis.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><code>charges<\/code> is a quoted string, and it is a third spelling<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>\"0.50\"<\/code>. Two decimal places, quoted. SMS calls the same concept <code>amount<\/code> and returns an unquoted float. RCS calls it <code>amount<\/code> and returns a quoted string with four decimals. Parse to a decimal type, never a float, and write one adapter per channel.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><code>sendMethod<\/code> is ambiguous here and <code>channel<\/code> is not<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The row carries <code>sendMethod: 1<\/code> with no explanation on the delivery report page. The summary endpoint documents <code>sendMethod<\/code> as a direction filter where <code>1<\/code> is outgoing and <code>2<\/code> is incoming, which strongly suggests the same meaning on this row, but the delivery page does not say so. Meanwhile <code>channel: 1<\/code> with <code>channelName: \"API\"<\/code> is documented plainly: <code>1<\/code> API, <code>2<\/code> UI, <code>3<\/code> Agent.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Use <code>channel<\/code> and <code>channelName<\/code> when you want to know where a message came from. They are documented, they come as a matched code and label pair, and the label saves you a lookup table. Leave <code>sendMethod<\/code> alone on this row.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Worth noting in passing that <code>sendMethod<\/code> now means three unrelated things across the platform: a request parameter with string values on SMS and RCS sends, an undocumented integer on the Telegram delivery row, and a direction filter on the Telegram summary.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Filters<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The available filters are generous: <code>fromDate<\/code> and <code>toDate<\/code> on submit time with <code>from<\/code> and <code>to<\/code> aliases, <code>chatId<\/code>, <code>uuId<\/code> aliased to <code>transactionId<\/code>, <code>uniqueId<\/code>, <code>msgId<\/code>, <code>msgType<\/code>, <code>channel<\/code>, <code>phoneNumber<\/code>, <code>page<\/code> and <code>limit<\/code>. Results come back newest first by <code>submitTime<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">There is one filter you should not use. <code>status<\/code> is documented as an alias for <code>globalErrorCode<\/code>. In a response where <code>status<\/code> at top level means whether the API call succeeded, accepting <code>status<\/code> as a request parameter meaning something entirely different is an invitation to a confusing bug. Write <code>globalErrorCode=1<\/code> when you want failures. It is longer and it is unambiguous.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"type-flip\" class=\"wp-block-heading\">The Payload Type Flip on Error<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This is the finding that will cost you the most if you meet it in production rather than reading it here.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Line up the error responses across the family:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{ \"status\": \"error\", \"setup\": &#91;],          \"statusCode\": \"403\", \"reason\": \"...\" }\n{ \"status\": \"error\", \"templatesList\": &#91;],  \"statusCode\": \"403\", \"reason\": \"...\" }\n{ \"status\": \"error\", \"recipientsList\": &#91;], \"statusCode\": \"403\", \"reason\": \"...\" }\n{ \"status\": \"error\", \"inboxList\": &#91;],      \"statusCode\": \"403\", \"reason\": \"...\" }\n{ \"status\": \"error\", \"dlrList\": &#91;],        \"statusCode\": \"403\", \"reason\": \"...\" }\n{ \"status\": \"error\", \"summaryList\": &#91;],    \"statusCode\": \"403\", \"reason\": \"...\" }\n{ \"status\": \"error\", \"analytics\": &#91;],      \"statusCode\": \"403\", \"reason\": \"...\" }<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">For the list shaped endpoints this is unremarkable: an empty array where an array was expected. For <code>setup<\/code> and <code>analytics<\/code> it is not. Both return a JSON <strong>object<\/strong> on success and an <strong>empty array<\/strong> on error. The templates endpoint does the same thing on <code>action=get<\/code>, which the page states directly: the error payload uses an empty <code>template<\/code> array instead of the object.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This is the classic PHP serialisation artifact, where an empty associative array encodes as <code>[]<\/code> rather than <code>{}<\/code>. Understanding why it happens does not make it go away, and the consequence is concrete. In Go, <code>json.Unmarshal<\/code> of <code>[]<\/code> into a struct returns an error. In Rust, serde fails the same way. In Java with Jackson, or C# with System.Text.Json, you get a deserialisation exception. In every one of those languages, your client works perfectly against a healthy account and blows up with a parse error the first time the account hits a 403, which means the error you actually see in your logs is a JSON error rather than &#8220;Telegram API access is not enabled for this account.&#8221;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The correct client shape, and you should apply it to all eight endpoints without exception:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Parse the response into a loose representation first: <code>map[string]any<\/code>, <code>serde_json::Value<\/code>, <code>JsonNode<\/code>, <code>dict<\/code>.<\/li>\n\n\n\n<li>Read the top level <code>status<\/code> string. Nothing else.<\/li>\n\n\n\n<li>If it is not <code>\"success\"<\/code>, read <code>reason<\/code> on the seven read endpoints or <code>message<\/code> on <code>\/send<\/code>, log it with <code>statusCode<\/code>, and return. Do not touch the payload key.<\/li>\n\n\n\n<li>Only on success, bind the payload key into your typed structure.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">In Go specifically, declare the payload field as <code>json.RawMessage<\/code> and unmarshal it in step four. In Rust, <code>serde_json::Value<\/code>. In Jackson, <code>JsonNode<\/code>. That two stage parse costs you about ten lines and it makes every failure in the family legible.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Python and JavaScript will not crash, which is a mixed blessing: they will hand you an empty list where your code expects a dictionary, and you will get an <code>AttributeError<\/code> or a silent <code>undefined<\/code> somewhere further downstream and further from the cause. The two stage parse is worth writing in those languages too.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"three-way-comparison\" class=\"wp-block-heading\">Telegram, SMS and RCS Side by Side<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The platform now runs three outbound channels with independently designed contracts. This is the comparison nobody has written down, and it is the reason a shared deserialiser is not merely inelegant but incorrect.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Concept<\/th><th>SMS<\/th><th>RCS<\/th><th>Telegram<\/th><\/tr><\/thead><tbody><tr><td>Send base path<\/td><td><code>SMSApi\/send<\/code><\/td><td><code>RCSApi\/send<\/code><\/td><td><code>rest\/tg\/v1\/send<\/code><\/td><\/tr><tr><td>Format parameter<\/td><td><code>output<\/code><\/td><td><code>format<\/code><\/td><td><code>output<\/code><\/td><\/tr><tr><td>Recipient identifier<\/td><td><code>mobile<\/code><\/td><td><code>mobile<\/code><\/td><td><code>chatId<\/code> or <code>phoneNumber<\/code><\/td><\/tr><tr><td>Sender identity<\/td><td>Sender ID, 6 chars<\/td><td><code>botId<\/code><\/td><td>The one account bot, implicit<\/td><\/tr><tr><td>Identity is selectable per send<\/td><td>Yes<\/td><td>Yes<\/td><td>No<\/td><\/tr><tr><td>Send response shape<\/td><td>Union of four, keyed on <code>sendMethod<\/code><\/td><td>One shape, all methods<\/td><td>One shape<\/td><\/tr><tr><td>Send response has <code>statusCode<\/code><\/td><td>Yes<\/td><td>Yes<\/td><td><strong>No<\/strong><\/td><\/tr><tr><td>Send response text field<\/td><td><code>reason<\/code><\/td><td><code>reason<\/code><\/td><td><code>message<\/code><\/td><\/tr><tr><td>Per recipient outcome on send<\/td><td><code>invalidMobile<\/code><\/td><td><code>invalidMobile<\/code><\/td><td><strong>None<\/strong><\/td><\/tr><tr><td>Caller supplied correlation token<\/td><td>None<\/td><td><code>identifier<\/code><\/td><td>None<\/td><\/tr><tr><td>Scheduling parameter<\/td><td><code>scheduleTime<\/code><\/td><td>None<\/td><td>None<\/td><\/tr><tr><td>Template required<\/td><td>In India, yes<\/td><td>Yes<\/td><td>No<\/td><\/tr><tr><td>Placeholder syntax<\/td><td><code>{#var#}<\/code><\/td><td><code>[name]<\/code><\/td><td><code>{name}<\/code><\/td><\/tr><tr><td>Delivery outcome field<\/td><td><code>status<\/code><\/td><td><code>deliveryStatus<\/code><\/td><td><code>deliveryStatus<\/code><\/td><\/tr><tr><td>Outcome value casing<\/td><td>Uppercase<\/td><td>Uppercase<\/td><td>Lowercase<\/td><\/tr><tr><td><code>globalErrorCode<\/code> space<\/td><td>Rich taxonomy<\/td><td>Rich, e.g. <code>5007<\/code><\/td><td>Binary, <code>0<\/code> or <code>1<\/code><\/td><\/tr><tr><td>Terminality flag<\/td><td>None<\/td><td>None<\/td><td><strong><code>isFinal<\/code><\/strong><\/td><\/tr><tr><td>Cost field name<\/td><td><code>amount<\/code><\/td><td><code>amount<\/code><\/td><td><code>charges<\/code><\/td><\/tr><tr><td>Cost JSON type<\/td><td>Unquoted float<\/td><td>Quoted string, 4 dp<\/td><td>Quoted string, 2 dp<\/td><\/tr><tr><td>Cost semantics<\/td><td>Per part<\/td><td>Per message<\/td><td>Per message<\/td><\/tr><tr><td>Recipient number JSON type<\/td><td>Unquoted number<\/td><td>Quoted string<\/td><td>Quoted string<\/td><\/tr><tr><td>Timestamps per delivery row<\/td><td>2<\/td><td>4<\/td><td>3<\/td><\/tr><tr><td>Read and click tracking<\/td><td>No<\/td><td>Yes<\/td><td>No<\/td><\/tr><tr><td>Paging model<\/td><td>Opaque cursor<\/td><td><code>page<\/code> and <code>limit<\/code><\/td><td><code>page<\/code> and <code>limit<\/code><\/td><\/tr><tr><td>Delivery push to your server<\/td><td>One account webhook<\/td><td>Not documented<\/td><td>Not documented<\/td><\/tr><tr><td>Summary counter JSON type<\/td><td>Quoted strings<\/td><td>Unquoted integers<\/td><td>Unquoted integers<\/td><\/tr><tr><td>Summary covers inbound<\/td><td>No<\/td><td>No<\/td><td><strong>Yes<\/strong><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Two rows in that table deserve emphasis for anyone running more than one channel.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Telegram is the only channel that tells you a row is finished.<\/strong> If you are building a unified reconciler, that is a capability you can use on one third of your traffic and must emulate on the rest.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Telegram is the only summary that mixes directions.<\/strong> <code>sendMethod<\/code> <code>1<\/code> is outgoing and <code>2<\/code> is incoming, and both appear in the same list. Summing <code>total<\/code> across unfiltered rows gives you a number that means nothing. Always pass <code>sendMethod=1<\/code> when you want outbound volume.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"inbox-and-webhook\" class=\"wp-block-heading\">The Inbox and the Webhook That Is Not Yours<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><code>GET https:\/\/unify.smsgateway.center\/rest\/tg\/v1\/inbox<\/code><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Here is a sentence that catches people out. <code>\/setup<\/code> registers a webhook. That webhook is registered <strong>with Telegram, pointing at SMSGatewayCenter<\/strong>. It is how the platform receives inbound messages. It is not a push to your application.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The inbox endpoint is explicitly read only, and there is no documented outbound push for this channel. Inbound <a href=\"https:\/\/www.smsgatewaycenter.com\/telegram-bulk-messaging\/\">Telegram messaging<\/a> is a polling integration, and you should design it as one rather than waiting for a callback that is not coming.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The row:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n    \"incomingId\": \"101\",\n    \"chatId\": \"123456789\",\n    \"telegramUserId\": \"987654321\",\n    \"firstName\": \"Rahul\",\n    \"username\": \"rahul_demo\",\n    \"phoneNumber\": \"919999999999\",\n    \"message\": \"Hello\",\n    \"msgType\": \"text\",\n    \"mediaUrl\": \"\",\n    \"receivedAt\": 1712345678901,\n    \"receivedAtFormatted\": \"2026-07-07 10:14:38\",\n    \"isRead\": 0\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>message<\/code> is plain text here, which is worth noting against RCS, where the equivalent field holds double encoded JSON. <code>mediaUrl<\/code> is populated when someone sends you a picture or a file, so an inbound handler needs a media branch. <code>msgType<\/code> mirrors the outbound type vocabulary.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Filters: <code>fromDate<\/code> and <code>toDate<\/code>, <code>chatId<\/code>, <code>telegramUserId<\/code>, <code>phoneNumber<\/code>, <code>isRead<\/code>, <code>page<\/code> and <code>limit<\/code>. Newest first by <code>receivedAt<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Note what <code>isRead<\/code> is and is not. It is a filterable field and a row value. There is no endpoint in this family that sets it. Something else, presumably the panel, owns that flag. So <code>isRead<\/code> tells you what a human in the panel has looked at, which is a different question from what your integration has processed.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Build the poller on your own cursor. Keep the highest <code>receivedAt<\/code> you have successfully handled, poll from slightly before it to allow for clock skew and late arrivals, deduplicate on <code>incomingId<\/code>, and advance only after your handler has committed. Four moving parts:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>A window.<\/strong> From <code>last_processed_at<\/code> minus a small overlap, to now.<\/li>\n\n\n\n<li><strong>A page loop.<\/strong> <code>limit=200<\/code>, increment <code>page<\/code> until you have fewer rows than the limit.<\/li>\n\n\n\n<li><strong>A dedupe.<\/strong> <code>incomingId<\/code> is the natural key. A unique index on it makes the poller idempotent and lets you run two instances without coordination.<\/li>\n\n\n\n<li><strong>A commit.<\/strong> Advance <code>last_processed_at<\/code> only after handling succeeds, so a crash replays rather than skips.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Replying is a send. <code>\/inbox<\/code> gives you the <code>chatId<\/code>, <code>\/send<\/code> takes it, and the conversation continues. There is no threading concept and no reply-to parameter, so a reply is simply a new message to the same chat.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"summary-and-analytics\" class=\"wp-block-heading\">Summary and Analytics: Which Number to Trust<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Two aggregate endpoints exist and they answer different questions.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>GET \/rest\/tg\/v1\/summary<\/code> returns bucketed counters:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n    \"summaryId\": \"88\",\n    \"sendMethod\": 1,\n    \"sendMethodName\": \"Outgoing\",\n    \"total\": 120,\n    \"success\": 115,\n    \"failed\": 3,\n    \"pending\": 2,\n    \"summaryTime\": 1712345678901,\n    \"summaryTimeFormatted\": \"2026-07-07 10:14:38\"\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Counters are unquoted integers, which matches RCS and differs from the SMS summary where they are quoted strings except for <code>pending<\/code>. <code>sendMethod<\/code> filters direction. <code>summaryId<\/code> is a quoted string.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>GET \/rest\/tg\/v1\/analytics<\/code> returns dashboard shaped data across five actions: <code>overview<\/code>, <code>today<\/code>, <code>messagebreakdown<\/code>, <code>subscriberstats<\/code> and <code>subscribertrend<\/code>. Date ranges cannot exceed 31 days, the same cap the SMS dashboard endpoint carries.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The analytics payload contains the most interesting number in the family, and also the clearest evidence of where these endpoints came from:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\"messageBreakdown\": &#91;\n    { \"label\": \"Submitted\", \"value\": 120, \"labelColor\": \"#0B62A4\" },\n    { \"label\": \"Delivered\", \"value\": 115, \"labelColor\": \"#01c0c8\" }\n]<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Those are hex colour codes in an API response. Along with <code>formMode<\/code> on the setup endpoint, they tell you these are the panel&#8217;s own data sources exposed as a public contract. The practical rule that follows applies to the whole family: <strong>bind the fields your code needs and ignore everything else.<\/strong> Do not assert on field counts, do not fail on unknown keys, and do not build a feature on a field whose purpose you cannot state.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Two more analytics notes worth acting on.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>total<\/code> means submitted.<\/strong> The page states it: total is submitted messages, success is delivered. That is the correct denominator and it matters. A delivery rate computed as delivered divided by delivered plus failed freezes at a healthy looking constant whenever reports stop arriving, because both terms stop moving together. Divide by submitted and a reporting outage shows up as a falling rate, which is what you want. This is the denominator trap covered at length in <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/observability-for-messaging-pipelines\/\">observability for messaging pipelines<\/a>, and the Telegram analytics endpoint hands you the right denominator by default.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>subscriberTrend<\/code> publishes the same series three times.<\/strong> There is a <code>labels<\/code> array, a parallel <code>counts<\/code> array, and a <code>series<\/code> array of objects each carrying <code>date<\/code>, <code>label<\/code> and <code>count<\/code>. Parallel arrays are an index alignment bug waiting to happen. Read <code>series<\/code> and ignore the other two.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>subscriberStats<\/code> is not date filtered and returns four fixed windows: <code>today<\/code>, <code>week<\/code>, <code>fifteenDays<\/code> and <code>month<\/code>. Subscriber counts are described as unique new Telegram customer mappings by creation date, which means this is your opt in growth rate. On a channel where the audience only grows by consent, that is the number that tells you whether the channel is becoming viable, and it is the one to put on a wall.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For daily reconciliation, derive your own totals from <code>\/dlr<\/code> rows you have ingested and use <code>\/summary<\/code> and <code>\/analytics<\/code> as an independent cross check. When your number and theirs disagree, you have found either an ingest gap or a bucketing difference, and either is worth knowing about before finance asks.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"endpoint-reference\" class=\"wp-block-heading\">Endpoint Reference Table<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">All paths relative to <code>https:\/\/unify.smsgateway.center\/rest\/tg\/v1\/<\/code>.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Endpoint<\/th><th>Method<\/th><th>Actions<\/th><th>Payload key<\/th><th>Purpose<\/th><\/tr><\/thead><tbody><tr><td><code>setup<\/code><\/td><td>GET, POST<\/td><td><code>get<\/code>, <code>save<\/code>, <code>registerwebhook<\/code><\/td><td><code>setup<\/code> (object)<\/td><td>Connect the bot, register the inbound webhook, read account flags and <code>tps<\/code><\/td><\/tr><tr><td><code>send<\/code><\/td><td>POST<\/td><td>none<\/td><td>none<\/td><td>Send one of eleven message types to chat IDs or a phone number<\/td><\/tr><tr><td><code>recipients<\/code><\/td><td>GET<\/td><td><code>list<\/code><\/td><td><code>recipientsList<\/code><\/td><td>Distinct inbox contacts for a date range<\/td><\/tr><tr><td><code>templates<\/code><\/td><td>GET<\/td><td><code>list<\/code>, <code>get<\/code><\/td><td><code>templatesList<\/code> \/ <code>template<\/code><\/td><td>List saved templates or fetch one compose payload<\/td><\/tr><tr><td><code>inbox<\/code><\/td><td>GET<\/td><td><code>list<\/code><\/td><td><code>inboxList<\/code><\/td><td>Incoming messages, read only<\/td><\/tr><tr><td><code>dlr<\/code><\/td><td>GET<\/td><td><code>list<\/code><\/td><td><code>dlrList<\/code><\/td><td>Delivery reports with eleven filters<\/td><\/tr><tr><td><code>summary<\/code><\/td><td>GET<\/td><td><code>list<\/code><\/td><td><code>summaryList<\/code><\/td><td>Bucketed counters, both directions<\/td><\/tr><tr><td><code>analytics<\/code><\/td><td>GET<\/td><td><code>overview<\/code>, <code>today<\/code>, <code>messagebreakdown<\/code>, <code>subscriberstats<\/code>, <code>subscribertrend<\/code><\/td><td><code>analytics<\/code> (object)<\/td><td>Dashboard metrics and subscriber growth<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Success status codes, tabulated the way you should tabulate every CRUD family on this platform:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Endpoint<\/th><th>Success <code>statusCode<\/code><\/th><th>Notes<\/th><\/tr><\/thead><tbody><tr><td><code>setup<\/code><\/td><td><code>\"200\"<\/code><\/td><td>Plus a nested <code>webhook.status<\/code><\/td><\/tr><tr><td><code>send<\/code><\/td><td><strong>absent<\/strong><\/td><td>Returns <code>status<\/code> and <code>message<\/code> only<\/td><\/tr><tr><td><code>recipients<\/code><\/td><td><code>\"200\"<\/code><\/td><td><\/td><\/tr><tr><td><code>templates<\/code><\/td><td><code>\"200\"<\/code><\/td><td>Both actions<\/td><\/tr><tr><td><code>inbox<\/code><\/td><td><code>\"200\"<\/code><\/td><td><\/td><\/tr><tr><td><code>dlr<\/code><\/td><td><code>\"200\"<\/code><\/td><td><\/td><\/tr><tr><td><code>summary<\/code><\/td><td><code>\"200\"<\/code><\/td><td><\/td><\/tr><tr><td><code>analytics<\/code><\/td><td><code>\"200\"<\/code><\/td><td><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Seven of eight publish <code>\"200\"<\/code> as a quoted string. The send endpoint publishes no status code at all. Compare RCS, where seven of eight publish <code>\"200\"<\/code> and <code>RCSApi\/template\/list<\/code> publishes no status field of any kind, and SMS, where <code>SMSApi\/group\/delete<\/code> returns <code>\"224\"<\/code> on success. Tabulating this per family takes five minutes and has caught a live anomaly in every family it has been applied to.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The documented account level error is uniform and useful:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{ \"status\": \"error\", \"dlrList\": &#91;], \"statusCode\": \"403\", \"reason\": \"Telegram API access is not enabled for this account.\" }<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Treat <code>403<\/code> as a configuration problem, not a transient one. Do not retry it.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"first-send\" class=\"wp-block-heading\">Your First Telegram Send in Six Steps<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Create the bot.<\/strong> Open BotFather in Telegram, run <code>\/newbot<\/code>, choose a name and a username, and copy the token it gives you. It looks like <code>123456789:AA...<\/code>. Put it straight into your secret manager.<\/li>\n\n\n\n<li><strong>Connect it.<\/strong> <code>POST \/rest\/tg\/v1\/setup<\/code> with <code>action=save<\/code>, <code>botName<\/code>, <code>botHandle<\/code> and <code>botToken<\/code>. Read the response. Confirm <code>isConfigured<\/code>, <code>isActive<\/code> and <code>apiAccessEnabled<\/code> are all <code>true<\/code>, and note the <code>tps<\/code> value.<\/li>\n\n\n\n<li><strong>Subscribe yourself.<\/strong> Open <code>https:\/\/t.me\/{botHandle}<\/code> on your own phone and send <code>\/start<\/code>. You are now the first row in your audience.<\/li>\n\n\n\n<li><strong>Find your chat ID.<\/strong> <code>GET \/rest\/tg\/v1\/recipients<\/code> with today&#8217;s date. Your <code>chatId<\/code> is in the list. This round trip is also the fastest way to confirm the inbound webhook is actually working, because your <code>\/start<\/code> only became a recipients row by arriving through it.<\/li>\n\n\n\n<li><strong>Send.<\/strong> <code>POST \/rest\/tg\/v1\/send<\/code> with <code>msgType=text<\/code>, your <code>chatId<\/code> and a short <code>text<\/code>. Save the <code>uuId<\/code> from the response.<\/li>\n\n\n\n<li><strong>Reconcile.<\/strong> <code>GET \/rest\/tg\/v1\/dlr<\/code> filtered by that <code>uuId<\/code>. Read <code>deliveryStatus<\/code>, <code>globalErrorCode<\/code> and <code>isFinal<\/code>. You have now exercised the entire loop, and you have a real payload to write your parser against rather than a sample from a page.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Step 6 is worth doing before you write any binding code at all. One real response is worth more than any published sample, because it tells you what your account actually returns rather than what the documentation says it should.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"working-code\" class=\"wp-block-heading\">Working Code in Four Languages<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Four short samples, each demonstrating a different trap rather than four translations of the same call.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">cURL: the baseline send<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>curl --location 'https:\/\/unify.smsgateway.center\/rest\/tg\/v1\/send' \\\n  --header 'Content-Type: application\/x-www-form-urlencoded' \\\n  --header 'apikey: YOUR_API_KEY' \\\n  --data-urlencode 'userid=YourUsername' \\\n  --data-urlencode 'msgType=text' \\\n  --data-urlencode 'chatId=123456789' \\\n  --data-urlencode 'text=Your order is confirmed.' \\\n  --data-urlencode 'output=json'<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Note <code>userid<\/code> is present even though the <code>apikey<\/code> header is. That is not redundancy, it is required.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Python: the two stage parse that survives a 403<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>import requests\n\nBASE = \"https:\/\/unify.smsgateway.center\/rest\/tg\/v1\"\n\ndef tg_get(path, api_key, userid, **params):\n    params.update({\"userid\": userid, \"output\": \"json\"})\n    r = requests.get(f\"{BASE}\/{path}\", params=params,\n                     headers={\"apikey\": api_key}, timeout=30)\n    r.raise_for_status()\n    body = r.json()\n\n    # Stage one: read only the status string.\n    if body.get(\"status\") != \"success\":\n        raise RuntimeError(\n            f\"{path} failed: {body.get('reason')} \"\n            f\"(statusCode={body.get('statusCode')})\"\n        )\n\n    # Stage two: only now is the payload key safe to touch.\n    return body\n\nreports = tg_get(\"dlr\", API_KEY, USERID,\n                 fromDate=\"2026-07-01\", toDate=\"2026-07-07\",\n                 globalErrorCode=1, limit=200)\n\nfor row in reports&#91;\"dlrList\"]:\n    print(row&#91;\"uuId\"], row&#91;\"deliveryStatus\"], row&#91;\"isFinal\"])<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Without the status check, a 403 hands you <code>{\"dlrList\": []}<\/code> and the loop runs zero times in silence. Zero failures and an access problem look identical.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Go: the raw message that stops the unmarshal from exploding<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>type tgEnvelope struct {\n    Status     string          `json:\"status\"`\n    StatusCode string          `json:\"statusCode\"`\n    Reason     string          `json:\"reason\"`\n    Setup      json.RawMessage `json:\"setup\"`\n}\n\ntype tgSetup struct {\n    IsConfigured     bool   `json:\"isConfigured\"`\n    IsActive         bool   `json:\"isActive\"`\n    APIAccessEnabled bool   `json:\"apiAccessEnabled\"`\n    BotChatURL       string `json:\"botChatUrl\"`\n    TPS              int    `json:\"tps\"`\n}\n\nfunc readSetup(body &#91;]byte) (*tgSetup, error) {\n    var env tgEnvelope\n    if err := json.Unmarshal(body, &amp;env); err != nil {\n        return nil, err\n    }\n    if env.Status != \"success\" {\n        return nil, fmt.Errorf(\"setup: %s (%s)\", env.Reason, env.StatusCode)\n    }\n    var s tgSetup\n    if err := json.Unmarshal(env.Setup, &amp;s); err != nil {\n        return nil, err\n    }\n    return &amp;s, nil\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Declaring <code>Setup<\/code> as a struct instead of <code>json.RawMessage<\/code> compiles, passes every test against a working account, and fails on the 403 with a type error that names nothing useful. The booleans are real booleans, so <code>bool<\/code> is correct here and would not be on any other channel.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">PHP: sending a photo without colliding the two phoneNumber fields<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>$mediaPayload = json_encode(&#91;\n    'mediaUrl' =&gt; 'https:\/\/cdn.example.com\/receipts\/inv-8841.jpg',\n]);\n\n$fields = &#91;\n    'userid'       =&gt; $userid,\n    'password'     =&gt; $password,\n    'msgType'      =&gt; 'photo',\n    'chatId'       =&gt; $chatId,          \/\/ recipient\n    'text'         =&gt; 'Your invoice is attached.',  \/\/ caption: 1024 max\n    'mediaPayload' =&gt; $mediaPayload,\n    'output'       =&gt; 'json',\n];\n\n$ch = curl_init('https:\/\/unify.smsgateway.center\/rest\/tg\/v1\/send');\ncurl_setopt_array($ch, &#91;\n    CURLOPT_POST           =&gt; true,\n    CURLOPT_POSTFIELDS     =&gt; http_build_query($fields),\n    CURLOPT_RETURNTRANSFER =&gt; true,\n    CURLOPT_TIMEOUT        =&gt; 30,\n]);\n$body = json_decode(curl_exec($ch), true);\ncurl_close($ch);\n\nif (($body&#91;'status'] ?? '') !== 'success') {\n    throw new RuntimeException($body&#91;'message'] ?? 'unknown telegram error');\n}\n$uuId = $body&#91;'uuId'];   \/\/ the quoted one. Never $body&#91;'uniqueId'].<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Two things in eleven lines: the caption limit is 1024 and not 4096 because <code>msgType<\/code> is <code>photo<\/code>, and the error branch reads <code>message<\/code> rather than <code>reason<\/code> because this is the send endpoint. Both are easy to get wrong and neither produces an obvious symptom.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For the wider platform, the <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/sms-api-php-integration-tutorial\/\">PHP<\/a> and <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/sms-api-python-integration-tutorial\/\">Python<\/a> integration tutorials cover the SMS surface in depth. The official SDKs do not currently cover the Telegram endpoints, so the calls above are the reference implementation.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"ten-mistakes\" class=\"wp-block-heading\">Ten Mistakes That Cost a Sprint<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Binding the payload key to a struct in a typed language.<\/strong> Works until the first 403, then fails with a JSON error that hides the real message. Two stage parse.<\/li>\n\n\n\n<li><strong>Reconciling on <code>uniqueId<\/code>.<\/strong> The delivery report only guarantees <code>uuId<\/code> and <code>transactionId<\/code> match what you sent. Reconcile on <code>uuId<\/code>.<\/li>\n\n\n\n<li><strong>Reading <code>status<\/code> from a delivery row.<\/strong> There is no <code>status<\/code> on the row. The outcome is <code>deliveryStatus<\/code>, and <code>status<\/code> at envelope level is about the API call.<\/li>\n\n\n\n<li><strong>Sharing a <code>globalErrorCode<\/code> mapping across channels.<\/strong> Binary on Telegram, rich taxonomy on SMS and RCS. Key on channel plus code.<\/li>\n\n\n\n<li><strong>Treating <code>\/recipients<\/code> as a subscriber table.<\/strong> It is a date ranged query over inbound messages. Keep your own table.<\/li>\n\n\n\n<li><strong>Sending to many chat IDs in one call and assuming all landed.<\/strong> One identifier comes back for N recipients and there is no per recipient result.<\/li>\n\n\n\n<li><strong>Composing a message body, then attaching an image.<\/strong> The limit drops from 4096 to 1024 when <code>msgType<\/code> stops being <code>text<\/code>.<\/li>\n\n\n\n<li><strong>Using <code>chatId<\/code> and <code>telegramUserId<\/code> interchangeably.<\/strong> Different numbers, and only <code>chatId<\/code> is addressable.<\/li>\n\n\n\n<li><strong>Waiting for a delivery webhook.<\/strong> The webhook in <code>\/setup<\/code> points at the platform, not at you. Poll <code>\/dlr<\/code>.<\/li>\n\n\n\n<li><strong>Summing summary rows without filtering direction.<\/strong> Incoming and outgoing share the list. Pass <code>sendMethod=1<\/code>.<\/li>\n<\/ol>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"decision-matrix\" class=\"wp-block-heading\">Choosing Between Telegram, SMS and RCS<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Dimension<\/th><th>Choose SMS<\/th><th>Choose RCS<\/th><th>Choose Telegram<\/th><\/tr><\/thead><tbody><tr><td>Recipient has not interacted with you<\/td><td>Yes<\/td><td>Yes<\/td><td>No, impossible<\/td><\/tr><tr><td>Guaranteed reach on any handset<\/td><td>Yes<\/td><td>No<\/td><td>No<\/td><\/tr><tr><td>Regulated OTP and transactional alerts in India<\/td><td>Yes<\/td><td>No<\/td><td>No<\/td><\/tr><tr><td>Rich media in the message<\/td><td>No<\/td><td>Yes<\/td><td>Yes<\/td><\/tr><tr><td>Interactive buttons<\/td><td>No<\/td><td>Yes<\/td><td>Yes<\/td><\/tr><tr><td>Polls and surveys<\/td><td>Via long code<\/td><td>No<\/td><td>Yes, natively<\/td><\/tr><tr><td>Read and click tracking<\/td><td>No<\/td><td>Yes<\/td><td>No<\/td><\/tr><tr><td>Two way conversation<\/td><td>Via long code<\/td><td>Poll the inbox<\/td><td>Poll the inbox<\/td><\/tr><tr><td>Per message carrier fee<\/td><td>Yes<\/td><td>Yes<\/td><td>No Telegram fee<\/td><\/tr><tr><td>Template approval before sending<\/td><td>Required in India<\/td><td>Required<\/td><td>Not required<\/td><\/tr><tr><td>Message length<\/td><td>160 per segment<\/td><td>No segment concept<\/td><td>4096, or 1024 as a caption<\/td><\/tr><tr><td>Terminal status is explicit<\/td><td>No<\/td><td>No<\/td><td>Yes, <code>isFinal<\/code><\/td><\/tr><tr><td>Scheduling through the API<\/td><td>Yes<\/td><td>No<\/td><td>No<\/td><\/tr><tr><td>Good for a school parent broadcast<\/td><td>Workable, costs add up<\/td><td>Handset dependent<\/td><td>Strong fit<\/td><\/tr><tr><td>Good for a password reset code<\/td><td>Strong fit<\/td><td>No<\/td><td>No<\/td><\/tr><tr><td>Good for an order status update<\/td><td>Strong fit<\/td><td>Strong fit<\/td><td>Strong fit if subscribed<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The honest summary: Telegram is excellent for ongoing communication with a community that has chosen to hear from you, and useless for reaching someone who has not. It complements SMS rather than replacing it, and the sensible architecture routes critical universal messages to SMS and routine community messages to Telegram, with <a href=\"https:\/\/www.smsgatewaycenter.com\/multi-channel-fallback\/\">multi channel fallback<\/a> expressing the preference once rather than in every call site.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"production-checklist\" class=\"wp-block-heading\">Production Checklist<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Setup and identity<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Non production uses a separate account, because the bot is account scoped<\/li>\n\n\n\n<li>All eight endpoints called with the <code>apikey<\/code> header, never with a password in a query string<\/li>\n\n\n\n<li>Bot token stored in a secret manager, never logged, never read back<\/li>\n\n\n\n<li><code>save<\/code> request bodies excluded from request logging<\/li>\n\n\n\n<li>Deployment smoke test asserts <code>isConfigured<\/code>, <code>isActive<\/code> and <code>apiAccessEnabled<\/code><\/li>\n\n\n\n<li><code>tps<\/code> read at startup and used to size the sender&#8217;s rate limiter<\/li>\n\n\n\n<li><code>maxAgents<\/code> checked against how many support seats you expect<\/li>\n\n\n\n<li><code>botChatUrl<\/code> rendered as a QR code client side<\/li>\n\n\n\n<li>A documented runbook step calls <code>registerwebhook<\/code> if inbound messages stop<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Data model<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>telegram_subscriber<\/code> table keyed on <code>chat_id<\/code> as a string<\/li>\n\n\n\n<li><code>telegram_user_id<\/code> stored separately and never used for sending<\/li>\n\n\n\n<li><code>first_seen_at<\/code> and <code>last_seen_at<\/code> maintained by the sweep<\/li>\n\n\n\n<li>Backfill sweep over <code>\/recipients<\/code> from bot go-live date completed<\/li>\n\n\n\n<li>Nightly sweep job scheduled and monitored<\/li>\n\n\n\n<li>Outbound table stores <code>uuId<\/code> as the correlation key<\/li>\n\n\n\n<li><code>logId<\/code> and <code>msgId<\/code> stored for support lookups<\/li>\n\n\n\n<li>All identifiers stored as text, never integers<\/li>\n\n\n\n<li><code>charges<\/code> parsed into a decimal type, never a float<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Sending<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>One recipient per call wherever per message accounting matters<\/li>\n\n\n\n<li>Length validated against 4096 or 1024 depending on <code>msgType<\/code><\/li>\n\n\n\n<li><code>chatId<\/code> and <code>phoneNumber<\/code> mutually exclusive, enforced before the call<\/li>\n\n\n\n<li>Contact card <code>phoneNumber<\/code> namespaced separately from recipient <code>phoneNumber<\/code><\/li>\n\n\n\n<li>Media URLs public, HTTPS, and stable for longer than the fetch window<\/li>\n\n\n\n<li><code>replyMarkup<\/code> validated against the Telegram Bot API format<\/li>\n\n\n\n<li>Send errors read <code>message<\/code>, not <code>reason<\/code><\/li>\n\n\n\n<li>Rate limiter honours the <code>tps<\/code> value from setup<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Reporting<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Two stage parse implemented on all eight endpoints<\/li>\n\n\n\n<li><code>403<\/code> treated as configuration, not retried<\/li>\n\n\n\n<li>Reconciler stops polling when <code>isFinal<\/code> is truthy<\/li>\n\n\n\n<li><code>globalErrorCode<\/code> mapping keyed on channel plus code<\/li>\n\n\n\n<li><code>deliveryStatus<\/code> compared case insensitively<\/li>\n\n\n\n<li><code>dispatchedTime<\/code> handled without assuming a <code>Formatted<\/code> twin<\/li>\n\n\n\n<li>Queue latency measured as <code>dispatchedTime<\/code> minus <code>submitTime<\/code><\/li>\n\n\n\n<li>Delivery rate computed with submitted as the denominator<\/li>\n\n\n\n<li><code>\/summary<\/code> queried with <code>sendMethod=1<\/code> for outbound totals<\/li>\n\n\n\n<li><code>subscriberTrend<\/code> read from <code>series<\/code>, not the parallel arrays<\/li>\n\n\n\n<li>Daily totals derived from ingested rows and cross checked against <code>\/summary<\/code><\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Inbox<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Poller keyed on <code>incomingId<\/code> with a unique index<\/li>\n\n\n\n<li>Cursor advanced only after the handler commits<\/li>\n\n\n\n<li>Overlap window on each poll to absorb clock skew<\/li>\n\n\n\n<li>Media branch handles a populated <code>mediaUrl<\/code><\/li>\n\n\n\n<li>Own processed flag maintained rather than relying on <code>isRead<\/code><\/li>\n\n\n\n<li>Replies sent to <code>chatId<\/code> from the inbox row<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"unspecified-behaviour\" class=\"wp-block-heading\">Unspecified Behaviour and How to Code Around It<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Some behaviours on this API are not pinned down by anything you can read. Each item below gives you the choice that stays correct whichever way the behaviour actually resolves, so none of them require you to wait for an answer before shipping.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>One. Send one recipient per call when you need per message accounting.<\/strong> A comma separated <code>chatId<\/code> list returns a single identifier and a single status, with no per recipient array. Whether the platform fans out internally and records N delivery rows, or treats the batch as one unit, is not something you want to discover during a reconciliation dispute. Loop in your own code, one call per chat ID, pace it with the <code>tps<\/code> value from setup, and every message gets its own <code>uuId<\/code>. Slower on the wire, and it makes your outbound table and the delivery report agree row for row. Use the comma separated form only for fire and forget notices where nobody will ever ask what happened to one particular person.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Two. Treat every timestamp as opaque until you have calibrated it once.<\/strong> No timezone is stated for any date field in the family, and the <code>Formatted<\/code> strings carry no offset. Send one message to yourself, note the wall clock time on your phone to the second, then read the row back and compare <code>submitTimeFormatted<\/code> against it. That single observation tells you the offset for every timestamp in the family. Until you have done it, store the raw millisecond epochs, which are unambiguous, and render the <code>Formatted<\/code> strings only as opaque display text. Never parse a <code>Formatted<\/code> string into a timestamp type and never use one in arithmetic.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Three. Persist <code>uuId<\/code> and reconcile on <code>uuId<\/code> only.<\/strong> The send response offers three names for what appears to be one value, and the delivery report guarantees only two of them match. Store all three columns if you like, as strings, but write exactly one join and write it on <code>uuId<\/code>. If the relationship between <code>uniqueId<\/code> and <code>uuId<\/code> is different from what a sample suggests, your reconciliation is unaffected.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Four. Cache the template body, do not resolve it at send time.<\/strong> A template is a saved compose payload rather than an enforced gate, and nothing documents what happens if the template changes between the moment you read it and the moment you send. Fetch with <code>action=get<\/code>, substitute your <code>{name}<\/code> placeholders locally, send the resulting literal text, and store the exact string you sent alongside the message. Your outbound record is then self describing and does not depend on a template that someone may edit next week.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Five. Guard every JSON-in-JSON field for emptiness before parsing.<\/strong> <code>mediaPayloadJson<\/code> and <code>buttonsJson<\/code> arrive as strings containing serialised JSON, and the documented sample shows both as empty strings. An empty string is not valid JSON and will throw in every language. Write one helper that returns an empty structure for null, empty and whitespace, and route both fields through it. Correct whether the platform sends <code>\"\"<\/code>, <code>null<\/code> or omits the key.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Six. Empty the meaning out of <code>sendMethod<\/code> on the delivery row.<\/strong> The field is present with value <code>1<\/code> and no explanation on the delivery report page, while the summary endpoint documents the same name as a direction filter. Use <code>channel<\/code> and <code>channelName<\/code> when you need to know where a message originated. Both are documented, they arrive as a matched code and label, and your code stays correct regardless of what <code>sendMethod<\/code> turns out to mean on that row.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Seven. Write <code>globalErrorCode=1<\/code> rather than <code>status=1<\/code> when filtering failures.<\/strong> The two are documented as aliases on the request, but <code>status<\/code> already means the API call outcome in the response of that same endpoint. Using the unambiguous spelling costs you nothing and removes a class of bug where a reader, or a future maintainer, mistakes a request filter for a response assertion.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Eight. Maintain your own processed marker for the inbox.<\/strong> <code>isRead<\/code> is filterable and appears on every row, and no endpoint in this family sets it. Whatever owns that flag, it is not answering the question your integration cares about. Track your own cursor and your own per message processed state, keyed on <code>incomingId<\/code>, and treat <code>isRead<\/code> as information about human activity in the panel rather than about your pipeline.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Nine. Derive daily totals from the rows you ingested, and use <code>\/summary<\/code> as a check.<\/strong> The summary bucket granularity is not stated and the documented <code>summaryTime<\/code> is not midnight, so the bucket may or may not be a calendar day. Sum your own ingested delivery rows for your reporting numbers, then compare against <code>\/summary<\/code> on a schedule. Agreement is reassuring; disagreement points at either an ingest gap or a bucketing difference, and you want to find both.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Ten. Bind only the fields you use, and fail on none that you do not.<\/strong> Hex colour codes and a <code>formMode<\/code> field sit in these responses today. Fields of that kind change without notice because they exist to serve a screen. Configure your deserialiser to ignore unknown properties, never assert on the shape of a response beyond the keys you read, and a cosmetic change on the platform side stays cosmetic on yours.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Eleven. Re-register the webhook as a routine recovery step, not an emergency one.<\/strong> <code>registerwebhook<\/code> exists as a standalone action, which tells you re-registration is an expected operation rather than a repair. Put it in your runbook: if inbound messages stop arriving and the account is otherwise healthy, call it once and re-check <code>\/inbox<\/code>. Cheap, safe to repeat, and it resolves the case where the registration lapsed without anyone noticing.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Twelve. Assume the bot token cannot be recovered, because it cannot.<\/strong> No endpoint returns it, and update calls accept its absence as &#8220;keep the current one&#8221;. If you lose the token, the recovery path runs through BotFather, not through this API. Store it where you store your other unrecoverable secrets and back it up accordingly.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"faqs\" class=\"wp-block-heading\">FAQs<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>What is the base URL for the SMSGatewayCenter Telegram API?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>https:\/\/unify.smsgateway.center\/rest\/tg\/v1\/<\/code> followed by the endpoint name: <code>setup<\/code>, <code>send<\/code>, <code>recipients<\/code>, <code>templates<\/code>, <code>inbox<\/code>, <code>dlr<\/code>, <code>summary<\/code> or <code>analytics<\/code>. A legacy alias without the <code>v1<\/code> segment is accepted on seven of the eight. Use the <code>v1<\/code> form.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Can I send a Telegram message to any phone number?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">No. Telegram addresses conversations, and a conversation exists only after the person has messaged your bot. You may send by <code>phoneNumber<\/code> instead of <code>chatId<\/code>, but only for contacts who shared their number with your bot, which again requires prior interaction. To reach someone cold, use SMS.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>How many Telegram bots can one account have?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">One. The send endpoint has no bot selector, and the message goes out on the bot linked to the account making the call. Separate environments need separate accounts.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>How do I get a chat ID?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Have the person open <code>https:\/\/t.me\/{yourBotHandle}<\/code> and send <code>\/start<\/code>. Then call <code>\/rest\/tg\/v1\/recipients<\/code> for that date range and read the <code>chatId<\/code> from the row. Store it in your own table; do not re-query it at send time.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>What is the difference between <code>chatId<\/code> and <code>telegramUserId<\/code>?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>chatId<\/code> identifies the conversation with your bot and is the only value you can send to. <code>telegramUserId<\/code> identifies the Telegram account and is useful as an inbox filter. They are different numbers and are not interchangeable.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Which identifier should I store to track a message?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>uuId<\/code>. It is a quoted string on both the send response and the delivery report, and the delivery report documentation confirms it matches what the send returned. <code>transactionId<\/code> carries the same value. Do not reconcile on <code>uniqueId<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Why does my strongly typed client crash only when something goes wrong?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Because the payload key changes JSON type on error. <code>setup<\/code> and <code>analytics<\/code> return an object on success and an empty array on error, and the list endpoints return an empty array in place of their rows. Parse the envelope loosely, check <code>status<\/code> first, and bind the payload only on success.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Is there a webhook that pushes Telegram delivery reports to my server?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Not a documented one. The webhook registered by <code>\/setup<\/code> points from Telegram to SMSGatewayCenter so the platform can receive inbound messages. Both delivery reports and inbound messages are polled by your application, from <code>\/dlr<\/code> and <code>\/inbox<\/code> respectively.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>How do I know when a delivery status is final?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Read <code>isFinal<\/code> on the delivery row. Telegram is the only channel on this platform that publishes an explicit terminality flag, so your reconciler can stop polling a row as soon as it is truthy instead of guessing from the status vocabulary.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Do Telegram messages need an approved template like DLT SMS?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">No. Templates on this channel are saved compose payloads you may reuse, not a compliance gate. You can send arbitrary text. Regulated Indian SMS is a different matter entirely and is covered in the <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/dlt-registration-step-by-step-guide-india\/\">DLT registration guide<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>What is the maximum message length?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">4096 characters for a <code>text<\/code> message, 1024 characters for a caption on a media message. The limit changes with <code>msgType<\/code>, so validate against the type you are about to send rather than against a constant.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>How many messages per second can I send?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Read the <code>tps<\/code> field from the <code>\/setup<\/code> response. It is an unquoted integer and reflects the rate configured on your account. Size your rate limiter from that value rather than hardcoding one.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Can I schedule a Telegram message through the API?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">No scheduling parameter is documented on <code>\/send<\/code>. Hold scheduled sends in your own job store and dispatch at the appropriate time.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>How does Telegram pricing compare with SMS?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Telegram does not charge a per message carrier fee for standard bot messaging, so the recurring cost is the SMSGatewayCenter platform fee under either a per message or a subscription model. SMS carries per message carrier and DLT costs, which is why the per SMS rate is only part of the picture, as explained in <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/bulk-sms-pricing-in-india-what-actually-drives-cost\/\">what actually drives bulk SMS cost<\/a>. Exact Telegram rates depend on your account, so ask for a quote on the <a href=\"https:\/\/www.smsgatewaycenter.com\/telegram-bulk-messaging\/\">Bulk Telegram Messaging page<\/a>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Building on Telegram alongside SMS or WhatsApp?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Telegram API access is enabled per account. <a href=\"https:\/\/www.smsgatewaycenter.com\/contact\/\">Get in touch<\/a> to have it switched on, or read the full <a href=\"https:\/\/www.smsgatewaycenter.com\/developer-api\/send-telegram-message\/\">Telegram developer documentation<\/a> to start integrating today.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">Recent Articles<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/rcs-messaging-api-reference-migration-from-sms\/\">RCS Messaging API: Complete Reference and Migration Path from SMS<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/delivery-report-ingestion-system-of-record\/\">Delivery Report Ingestion: Building a System of Record, Not a Dashboard<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/contact-groups-segments-data-model-group-send\/\">Contact Groups and Segments: The Data Model Behind a Group Send<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/campaign-splitting-one-send-several-transactions\/\">Campaign Splitting: What Happens When One Send Becomes Several Transactions<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/message-preview-cost-estimation-before-sending\/\">Message Preview and Cost Estimation: Showing Users What They Are About to Send<\/a><\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n","protected":false},"excerpt":{"rendered":"<p>A field by field reference for the SMSGatewayCenter Telegram REST API. Covers the eight endpoints under rest\/tg\/v1, the chat ID identity model, the three names one message identifier travels under, the payload type flip on error, and a three way comparison of the Telegram, SMS and RCS delivery rows. TABLE OF CONTENTS The Short Answer [&hellip;]<\/p>\n","protected":false},"author":118,"featured_media":2963,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[268],"tags":[2248,2245,2243,2242,2231,2239,27,2240,2244,2241],"class_list":["post-2962","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-tutorials","tag-api-reference","tag-bulk-telegram-messaging","tag-chat-id","tag-delivery-report","tag-messaging-api","tag-rest-api","tag-sms-gateway-center","tag-telegram-api","tag-telegram-bot","tag-two-way-messaging"],"_links":{"self":[{"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/posts\/2962","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/users\/118"}],"replies":[{"embeddable":true,"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/comments?post=2962"}],"version-history":[{"count":0,"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/posts\/2962\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/media\/2963"}],"wp:attachment":[{"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/media?parent=2962"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/categories?post=2962"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/tags?post=2962"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}