SMSGatewayCenter Blog

Sender Identity Across SMS, WhatsApp, RCS and Telegram: Sender IDs, WABA Numbers, Bots and Long Codes

A message on SMSGatewayCenter can come from five different kinds of sender: a DLT sender ID, a WhatsApp Business number, an RCS bot, the account's single Telegram bot, or a long code plus keyword on the way in. Each has its own API, its own spelling in responses, its own JSON type and its own approval model. This is the field-level map and the registry design that ties them together.

Featured image for Sender Identity Across SMS, WhatsApp, RCS and Telegram: Sender IDs, WABA Numbers, Bots and Long Codes
Five abstract sender shapes sending signal lines into one registry, representing SMS sender IDs, WhatsApp numbers, RCS bots, a Telegram bot and long codes
Five kinds of sender, one registry: the key you store decides whether a delivery report can be traced back to the brand that sent it.

Table of Contents

  1. The Short Answer
  2. TL;DR
  3. Five Sender Units at a Glance
  4. Who Issues, Who Approves, Who Can Change It
  5. SMS Sender IDs: A Mirror of the DLT Header
  6. Reading the Sender ID List
  7. Sender ID and Template Pairing
  8. WhatsApp: The WABA Number Is Sender and Scope
  9. RCS: The Bot Is the Sender
  10. Telegram: One Bot per Account
  11. Inbound SMS: Long Code Plus Keyword
  12. One Sender, Many Spellings
  13. Which Calls Each Sender Unit Scopes
  14. A Sender Registry Schema
  15. Resolving the Sender on Every Report and Inbox Row
  16. Four Code Samples, Four Traps
  17. Renaming, Retiring and Replacing a Sender
  18. Ten Mistakes That Break Sender Attribution
  19. Decision Matrix
  20. Pre-Launch Checklist
  21. Unspecified Behaviour and How to Code Around It
  22. FAQs

The Short Answer

Every channel on SMSGatewayCenter answers “who sent this?” with a different object. SMS uses a sender ID (the DLT header), managed through SMSApi/senderid/create, read, update and delete, sent as senderid and reported back as senderName. WhatsApp uses the WhatsApp Business number, wabaNumber, which is both the sender and the scope for templates, media, reports and inbox reads. RCS uses a bot, addressed by an integer botId on send but reported by botName on delivery rows. Telegram uses the single bot attached to the account, so the send call carries no sender parameter at all. Inbound SMS adds a fifth unit: the long code plus keyword pair that arrives as phonecode and keyword on the push.

The same sender appears under different names and JSON types across endpoints: the WhatsApp number is an unquoted number on report rows, a quoted string on inbox rows, and waNumber on analytics rows. The robust design is a sender registry in your own database with a surrogate key per sender, the channel identity stored as text, and a resolver that maps every report and inbox row back to that key.

TL;DR

  • Five sender units: SMS sender ID, WhatsApp wabaNumber, RCS botId, the account’s Telegram bot, and the inbound long code plus keyword.
  • SMS sender IDs have a full CRUD API under SMSApi/senderid/. Create takes only senderid and returns no identifier; read returns sId, senderName, isEnabled and addTime, all as quoted strings.
  • isEnabled is not a boolean. The sample value is the string "Pending", and the list response counts senders in total, pending, active and rejected buckets.
  • count on the sender list is an object, not a number. Do not share a paging helper with endpoints where count is an integer.
  • Sender and template must match on SMS. A mismatch fails the message with SENDERID_MISMATCH.
  • WhatsApp’s number changes type by endpoint: unquoted number on WAApi/report rows, quoted string on inbox rows, and renamed waNumber on analytics rows. Store it as text and normalise before comparing.
  • RCS reports the sender by name, not id. Delivery rows carry botName; inbox rows carry an integer botId; rest/rcs/v1/bots maps one to the other but returns active bots only.
  • Telegram has one bot per account. Nothing on a Telegram delivery row names the bot, and none needs to.
  • Design: a sender registry keyed on your own surrogate, one resolver per channel, and a daily snapshot of every sender list.

Five Sender Units at a Glance

The table puts the sender side of five endpoint families next to each other. Each row maps to a page on the SMSGatewayCenter developer API, so any cell can be checked against its source.

PropertySMS sender IDWhatsApp numberRCS botTelegram botInbound long code
What the recipient seesHeader such as SMSGATBusiness profile of the numberBranded botBot name and handleYour long code (you are the recipient)
Parameter on sendsenderidwabaNumberbotIdNoneNot applicable
Local identifiersId (quoted string)The number itselfbotId (integer)None exposedphonecode plus keyword
Managed through APICreate, read, update, deleteRead-only use; no createList only (rest/rcs/v1/bots)Setup get and savePortal
Approval signalisEnabled string, e.g. "Pending"Not exposedOnly active bots are listedNot applicableNot applicable
Name on delivery rowssenderNamewabaNumber (unquoted number)botNameNot presentNot applicable
Name on inbox rowsNot applicablewabaNumber (quoted string)botId (integer)Not presentphonecode, keyword
Five-column comparison of SMS sender ID, WhatsApp number, RCS bot, Telegram bot and inbound long code showing the send parameter, local identifier, approval signal and how each appears on reports
Five answers to “who sent this?”. No two columns use the same field name for the sender.

Read the last two rows first. Most attribution bugs start there: the field that identifies the sender on the way out is not the field that identifies it on the way back.


Who Issues, Who Approves, Who Can Change It

Sender unitIssued byApproved byChangeable through the API
SMS sender IDYou register the header on an operator DLT portalThe DLT portal, then the platform recordYes: create, update and delete under SMSApi/senderid/
WhatsApp numberProvisioned onto your accountMeta and the platform during onboardingNo; you use it, you do not manage it through the developer API
RCS botProvisioned onto your accountLaunched and activated before it appears in the bot listNo; list only
Telegram botYou create it with BotFather and save its tokenNo approval stepYes: rest/tg/v1/setup with save
Inbound long code and keywordAllocated by plan; keyword configured in the portalNot applicableNo

Two practical consequences. First, only SMS sender IDs and the Telegram bot can be created from your own code, so any “add a sender” feature in your product covers those two and routes everything else to an onboarding workflow. Second, the Telegram bot is the only sender with no approval gate, so a wrong token is live the moment you save it.

Telegram’s own BotFather documentation covers creating the bot and its token; the platform side is the setup endpoint described in the Telegram messaging API guide.


SMS Sender IDs: A Mirror of the DLT Header

In India, a commercial SMS header must be registered on an operator DLT portal before it can carry traffic. The platform’s sender ID record mirrors that registration: the update page describes the operation as a way to reflect changes “made or updated in DLT portals”. Register on DLT first; manage the platform copy through the API second. The DLT registration guide walks through the portal side.

The four endpoints

OperationEndpointMethodParameters beyond auth and outputSuccess msg
Createhttps://unify.smsgateway.center/SMSApi/senderid/createPOST (page also states GET)senderidSenderId created successfully.
Readhttps://unify.smsgateway.center/SMSApi/senderid/readPOST or GETnonesuccess
Updatehttps://unify.smsgateway.center/SMSApi/senderid/updatePOST onlysenderid, idSenderId updated successfully.
Deletehttps://unify.smsgateway.center/SMSApi/senderid/deletePOST onlyidSenderid deleted successfully

All four return code as the quoted string "200" nested under response, with api set to "senderid". Note the capitalisation of the word in the msg strings drifts between operations, which is one more reason to branch on status and never on msg.

Format rules to enforce before calling create

The create page describes Indian sender IDs as alphanumeric, up to six characters. Enforce that in your own validator before the call: uppercase letters and digits, one to six characters. A header that passes a six-character check is valid under any stricter reading, and the check turns a portal rejection into an immediate form error.

import re

SENDER_ID = re.compile(r"^[A-Z0-9]{1,6}$")

def validate_sender_id(value: str) -> str:
    v = value.strip().upper()
    if not SENDER_ID.fullmatch(v):
        raise ValueError("sender ID must be 1 to 6 letters or digits")
    return v

What the create does not return

The response to create confirms success and carries no sId. The only way to learn the identifier you need for update and delete is to read the list and match on senderName.


Reading the Sender ID List

{
  "response": {
    "api": "senderid",
    "action": "read",
    "status": "success",
    "msg": "success",
    "code": "200",
    "count": {
      "total": 1,
      "pending": 1,
      "active": 0,
      "rejected": 0
    },
    "senderidList": [
      {
        "senderid": {
          "sId": "179",
          "senderName": "TESTIN",
          "isEnabled": "Pending",
          "addTime": "1563033651005"
        }
      }
    ]
  }
}

Five things in that response decide how you parse it.

  1. count is an object with four buckets. On the SMS delivery report count is also an object but with total and current; on the template read it is an integer; on account status it counts keys. A shared helper that reads count as a number will throw here.
  2. Rows are double wrapped. Each element of senderidList holds a senderid object, so the name sits at response.senderidList[i].senderid.senderName. The word “senderid” is therefore a request parameter, a list prefix and a wrapper key all at once.
  3. isEnabled is a status string. Despite the name, the sample value is "Pending", and the bucket names suggest active and rejected states as well. Treat it as an enumeration, not a boolean.
  4. Everything is a quoted string, including sId and the epoch-millisecond addTime.
  5. There is no paging. The whole list comes back in one response.

Sender ID and Template Pairing

On SMS, the sender ID and the template are registered together on DLT. The platform enforces the pair at send time: the knowledge base entry for SENDERID_MISMATCH states the message was not sent because the sender ID does not match the one the template is registered with.

The pairing is visible from the API. Each row of SMSApi/template/read carries senderIds, a single string despite its plural name. One approved template can be linked to several sender IDs, as the knowledge base entry on using one template across multiple sender IDs confirms, provided each mapping exists on DLT and in your account.

The pre-send check is therefore a set membership test, not an equality test:

def allowed_senders(template_row: dict) -> set[str]:
    raw = template_row.get("senderIds", "") or ""
    return {s.strip().upper() for s in raw.split(",") if s.strip()}

def check_pair(template_row: dict, senderid: str) -> None:
    if senderid.upper() not in allowed_senders(template_row):
        raise ValueError(f"{senderid} is not mapped to template {template_row.get('mtId')}")

Splitting on commas is defensive: the published sample shows one value, and a list with one element is still a list. The full template model, including mtId and dltTemplateId, is covered in message template management across four channels.


WhatsApp: The WABA Number Is Sender and Scope

On WhatsApp the sender is the WhatsApp Business number, passed as wabaNumber. It does more work than any other sender field on the platform, because it also scopes most WhatsApp operations:

OperationHow wabaNumber is used
WAApi/sendRequired: the number the message comes from
WAApi/template readRequired: templates are listed per number
WAApi/template createRequired; delete identifies the template by name and language
WAApi/media uploadRequired; media reads are account-wide
WAApi/reportRequired filter; echoed on every row
rest/wa/v1/analyticsOptional filter; groupBy=waNumber groups rows by number
rest/wa/v1/inboxOptional filter; present on every row

The type of the value changes with the endpoint that returns it:

Where it appearsField nameJSON typeSample
WAApi/report rowwabaNumberUnquoted number9170396xxxxx
rest/wa/v1/inbox rowwabaNumberQuoted string"9170396xxxxx"
rest/wa/v1/analytics delivery rowwaNumberQuoted string"9170396xxxxx"

A twelve-digit Indian number fits safely in a JavaScript number, so the unquoted report value does not lose precision today. It still breaks equality: 9170396000001 === "9170396000001" is false. Normalise every occurrence to a digit string at the parser boundary and compare strings only. The report row also emits uuId as an unquoted nineteen-digit number, which does lose precision; the WhatsApp Business API wire contract covers that trap and the lossless parsing fix.

To build the list of numbers that are actually carrying traffic, call rest/wa/v1/analytics with action=delivery and groupBy=waNumber over a recent range. Each row names one number that sent messages in that window.


RCS: The Bot Is the Sender

On RCS the sender is a bot. The bot list endpoint is GET https://unify.smsgateway.center/rest/rcs/v1/bots (legacy alias /rest/rcs/bots), with userid, output=json and either password or the apikey header. Its only action is list.

{
  "status": "success",
  "botsList": [
    {
      "botId": 3,
      "name": "YourBot",
      "status": 1,
      "logoUrl": "https://example.com/bots/logo.png",
      "isSandbox": 0
    }
  ],
  "totalRecords": 1,
  "page": 1,
  "limit": 1,
  "fromDate": "",
  "toDate": "",
  "statusCode": "200",
  "reason": "success"
}

The notes on the page state three rules: only active bots (status equal to 1) are returned; use botId from this list in the send and template APIs; isSandbox of 1 marks a sandbox bot. On error, botsList becomes an empty array with a 403 and a reason such as “RCS API access is not enabled for this account.”

The complication is on the way back. The RCS delivery row identifies the sender by botName, a string, and carries no botId. The RCS inbox row identifies it by botId, an unquoted integer, and carries no name. The only bridge between the two is the bot list, and the bot list omits inactive bots. A delivery row from a bot that has since been deactivated cannot be mapped back through a live lookup. Snapshot the bot list daily into your registry, so a name that disappears from the live list still resolves from history.

Keep sandbox bots out of production routing by filtering on isSandbox at selection time, not by naming convention. The RCS messaging API reference covers the send payload that consumes botId.


Telegram: One Bot per Account

Telegram is the simplest case because there is only one sender. The account holds a single bot, configured through rest/tg/v1/setup: get (GET) returns the configuration; save (POST) accepts botName, botHandle and botToken. The token is write-only: it goes in and never comes back out on a read.

Three consequences:

  • The send call has no sender parameter. rest/tg/v1/send addresses recipients by chatId or phoneNumber; the bot is implied by the account.
  • Delivery and inbox rows carry no bot field. None is needed while the account holds exactly one bot, and your registry should record which bot handle was configured on which dates so historical rows stay attributable after a token rotation or a bot change.
  • Brands that need separate Telegram identities need separate accounts. Plan the account structure before you plan the bots.

Because the token cannot be read back, store it in your own secrets manager at the moment you save it, and treat a save as a deployment: a wrong token takes effect immediately and no approval step catches it.


Inbound SMS: Long Code Plus Keyword {#long-code-keyword}

The fifth sender unit points the other way. When a customer replies to a long code or texts a keyword, the platform forwards the message to your URL as an HTTP GET with six query parameters: phonecode, keyword, phoneno, content, location and carrier. phonecode and keyword are optional in the published contract; phoneno is the customer’s number and content is the text.

For routing, the pair of long code and keyword is your inbound “sender unit”: it tells you which campaign, brand or workflow a reply belongs to. Keywords are configured in the portal, as described in creating a new keyword for two-way SMS, and the full push contract is covered in receiving messages on four channels.

Normalise both values before routing: uppercase the keyword, strip whitespace, and treat a missing phonecode as “the account’s default long code” only if you hold exactly one. With more than one long code and no phonecode, route on keyword alone and send unmatched messages to a review queue rather than guessing.


One Sender, Many Spellings

ChannelWhereField nameJSON type
SMSSend requestsenderidForm field
SMSSender list rowsenderName (plus sId)Quoted string
SMSSender list wrappersenderidList[i].senderidObject
SMSTemplate rowsenderIdsQuoted string, plural name
SMSDelivery report rowsenderNameQuoted string
SMSDelivery summarygroupby=senderidRequest value
WhatsAppSend, template, media, report requestswabaNumberForm field
WhatsAppReport rowwabaNumberUnquoted number
WhatsAppInbox rowwabaNumberQuoted string
WhatsAppAnalytics rowwaNumberQuoted string
RCSSend and template list requestsbotIdForm or query field
RCSBot list rowbotId and nameInteger and string
RCSTemplate list rowbotNameQuoted string
RCSDelivery report rowbotNameQuoted string
RCSInbox rowbotIdUnquoted integer
TelegramSetupbotName, botHandle, botTokenStrings; token write-only
Inbound SMSPush query stringphonecode, keywordQuery parameters

Seventeen places, more than ten spellings, and at least four JSON representations. The fix is not to memorise the table but to encode it once: one small resolver per channel that reads the right field at the right nesting level, converts it to your canonical form, and returns your registry key.


Which Calls Each Sender Unit Scopes

OperationSMS sender IDWhatsApp numberRCS botTelegram bot
SendRequired (senderid)RequiredRequiredImplied
Template listNot a filter; pairing visible as senderIdsRequiredRequiredAccount-wide
Template createNot a parameterRequiredInside templateDataPortal only
Media uploadNot applicableRequiredNot applicableNot applicable
Delivery report queryNot among the query filters; echoed as senderName on each rowRequiredEchoed as botName on each rowAccount-wide
Summary or analyticsgroupby=senderid on the SMS summaryFilter and groupBy=waNumberGroup delivery rows by botName yourselfAccount-wide
InboxNot applicable (push)Optional filterEchoed as botIdAccount-wide

The asymmetry to design around: WhatsApp report queries require a number, so a job that reconciles all WhatsApp traffic has to loop over every number you hold. The SMS report query is account-wide and carries the sender on each row, so the same job makes one pass and groups afterwards. If your list of WhatsApp numbers is incomplete, the WhatsApp side of that job is silently incomplete too; the analytics groupBy=waNumber call described above is how you detect it.


A Sender Registry Schema

The registry is the table every send path reads to pick a sender and every ingestion path reads to attribute a row. PostgreSQL shown; the decisions carry to any relational store.

CREATE TABLE sender (
    sender_key        TEXT PRIMARY KEY,        -- your stable key, e.g. 'acme_alerts_sms'
    channel           TEXT NOT NULL CHECK (channel IN ('sms','whatsapp','rcs','telegram','sms_inbound')),
    brand             TEXT NOT NULL,           -- the business unit this sender speaks for

    -- channel identity, stored as text exactly as normalised
    sms_sender_id     TEXT,                    -- 'SMSGAT', uppercase
    sms_s_id          TEXT,                    -- sId, quoted string from the list
    waba_number       TEXT,                    -- digits only, never numeric
    rcs_bot_id        TEXT,                    -- integer stored as text
    rcs_bot_name      TEXT,                    -- the name delivery rows carry
    tg_bot_handle     TEXT,
    inbound_long_code TEXT,
    inbound_keyword   TEXT,                    -- uppercase

    -- state
    raw_status        TEXT,                    -- isEnabled, bot status, etc., verbatim
    is_sandbox        BOOLEAN NOT NULL DEFAULT FALSE,
    usable            BOOLEAN NOT NULL DEFAULT FALSE,   -- derived by the sync job only
    valid_from        TIMESTAMPTZ NOT NULL DEFAULT now(),
    valid_to          TIMESTAMPTZ,                      -- set when retired, never delete
    last_seen_at      TIMESTAMPTZ
);

CREATE UNIQUE INDEX ux_sms_sender  ON sender (sms_sender_id)  WHERE channel = 'sms' AND valid_to IS NULL;
CREATE UNIQUE INDEX ux_waba        ON sender (waba_number)    WHERE channel = 'whatsapp' AND valid_to IS NULL;
CREATE UNIQUE INDEX ux_rcs_bot     ON sender (rcs_bot_id)     WHERE channel = 'rcs' AND valid_to IS NULL;
CREATE INDEX        ix_rcs_name    ON sender (rcs_bot_name)   WHERE channel = 'rcs';
CREATE UNIQUE INDEX ux_inbound     ON sender (inbound_long_code, inbound_keyword) WHERE channel = 'sms_inbound' AND valid_to IS NULL;

CREATE TABLE sender_template (          -- SMS pairing, mirrored from template senderIds
    sender_key   TEXT REFERENCES sender(sender_key),
    template_key TEXT NOT NULL,
    PRIMARY KEY (sender_key, template_key)
);

Six decisions, and why:

  1. Surrogate key plus brand. Attribution questions are asked in business terms (“what did the alerts brand send?”), and a brand can own senders on several channels.
  2. Every identifier is text. sId arrives quoted, wabaNumber arrives both quoted and unquoted, and botId is an integer on one endpoint and absent on another. Text columns absorb all of it without loss.
  3. Validity intervals, not deletion. Delivery rows arrive for weeks after a send. A retired sender must still resolve, so rows are closed with valid_to, never deleted.
  4. rcs_bot_name is indexed but not unique. RCS delivery rows resolve by name. If a name is ever reused after a bot is replaced, the validity interval disambiguates by time.
  5. usable is derived. Only the sync job sets it, from the live lists: sender ID isEnabled indicating active, bot present in botsList with isSandbox matching the environment.
  6. The SMS pairing lives in its own table. It mirrors senderIds from the template read and backs the pre-send pair check.

The outbound side of this design is the outbound message table: store sender_key on every outbound row at submit time, and attribution never depends on a report field at all for messages you sent yourself.


Resolving the Sender on Every Report and Inbox Row

Flow from report and inbox rows on four channels, through a per-channel field extractor and normaliser, to a registry lookup by channel identity and time
Each channel names the sender differently on the way back. One resolver per channel turns that name into your sender key.

The resolution procedure, one channel at a time:

Step 1. Prefer your own record. If the row can be joined to your outbound table (by uuId or the composite key your ingestion uses), take sender_key from the outbound row and stop. This covers most outbound traffic.

Step 2. Extract the channel’s sender field.

RowField to readNormalise to
SMS delivery reportsenderNameUppercase, trimmed
WhatsApp reportwabaNumberDigit string
WhatsApp inboxwabaNumberDigit string
WhatsApp analyticswaNumberDigit string
RCS delivery reportbotNameExact string
RCS inboxbotIdInteger as string
Telegram dlr or inboxNoneThe account’s configured bot
SMS inbound pushphonecode, keywordDigits; uppercase keyword

Step 3. Look up the registry by identity and time. Match the normalised value against the channel’s identity column, restricted to rows where the event timestamp falls between valid_from and valid_to. Time matters for RCS names and for any sender that was retired and re-created.

Step 4. Handle a miss explicitly. Write the row with sender_key null and a sender_unresolved flag, and alert. Never guess from brand keywords in the message text.

Step 5. Backfill. When the sync job adds a sender (for example a WhatsApp number discovered through analytics), re-run resolution over unresolved rows in the retention window.


Four Code Samples, Four Traps

Every sample authenticates with the apikey header plus userid, which keeps passwords out of request bodies and access logs.

Python: SMS sender create returns no sId, and count is an object

import os
import requests

BASE = "https://unify.smsgateway.center/SMSApi/senderid"
USERID = os.environ["SGC_USERID"]
HEADERS = {"apikey": os.environ["SGC_APIKEY"]}

def _call(action: str, **fields) -> dict:
    r = requests.post(f"{BASE}/{action}",
                      data={"userid": USERID, "output": "json", **fields},
                      headers=HEADERS, timeout=30)
    r.raise_for_status()
    resp = r.json().get("response", {})
    if resp.get("status") != "success":
        raise RuntimeError(f"senderid/{action} failed: {resp}")
    return resp

def list_senders() -> tuple[dict, list[dict]]:
    resp = _call("read")
    buckets = resp.get("count") or {}          # an object: total, pending, active, rejected
    rows = [item["senderid"] for item in resp.get("senderidList", [])]
    return buckets, rows

def create_sender(name: str) -> dict:
    name = name.strip().upper()
    _call("create", senderid=name)
    _, rows = list_senders()
    matches = [r for r in rows if r.get("senderName", "").upper() == name]
    if len(matches) != 1:
        raise LookupError(f"expected one row for {name}, found {len(matches)}")
    row = matches[0]
    return {"sms_s_id": row["sId"], "sms_sender_id": name,
            "raw_status": row.get("isEnabled"), "added_ms": row.get("addTime")}

The traps: the identifier needed for update and delete only exists on the list, and count is a four-key object that a generic paging helper would misread.

Node.js: the WhatsApp number arrives as three types

// Normalise any wabaNumber / waNumber value to a digit string.
function normWaba(value) {
  if (value === null || value === undefined) return null;
  const s = String(value).replace(/\D/g, "");
  return s.length ? s : null;
}

// Report rows: quote wabaNumber, mobileNo and uuId before JSON.parse, so
// no numeric conversion ever happens.
function parseReportBody(text) {
  const safe = text.replace(
    /"(wabaNumber|mobileNo|uuId)"\s*:\s*(\d+)/g,
    (_m, key, digits) => `"${key}":"${digits}"`
  );
  return JSON.parse(safe);
}

function senderFromWhatsAppRow(row) {
  return normWaba(row.wabaNumber ?? row.waNumber);
}

The trap: strict equality between a number and a string is always false, so a registry lookup keyed on strings silently misses every report row unless the value is normalised first. Quoting the fields before parsing also protects the nineteen-digit uuId on the same row.

Java: RCS delivery rows name the bot, the bot list gives the id

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
import java.util.*;

public final class RcsBots {
    private static final ObjectMapper M = new ObjectMapper();
    private static final HttpClient HTTP = HttpClient.newHttpClient();

    /** Returns botName -> botId for currently active bots. Merge into a persisted snapshot. */
    public static Map<String, String> activeBots(String userid, String apikey) throws Exception {
        String qs = "userid=" + URLEncoder.encode(userid, StandardCharsets.UTF_8)
                  + "&output=json&action=list";
        HttpRequest req = HttpRequest.newBuilder(
                URI.create("https://unify.smsgateway.center/rest/rcs/v1/bots?" + qs))
            .header("apikey", apikey).GET().build();
        JsonNode root = M.readTree(HTTP.send(req, HttpResponse.BodyHandlers.ofString()).body());
        if (!"success".equals(root.path("status").asText())) {
            throw new IllegalStateException("bot list failed: " + root.path("reason").asText());
        }
        Map<String, String> byName = new HashMap<>();
        for (JsonNode b : root.path("botsList")) {       // bind only after success
            if (b.path("isSandbox").asInt() == 1) continue;
            byName.put(b.path("name").asText(), String.valueOf(b.path("botId").asLong()));
        }
        return byName;
    }
}

The trap: a live lookup misses every bot that is no longer active, so this method feeds a snapshot table rather than answering resolution queries directly. The error payload flips botsList to an empty array, which is why the status is read before the list is bound.

PHP: routing inbound SMS on long code plus keyword

<?php
// Registry loaded from your database: [longCode][KEYWORD] => sender_key
function route_inbound(array $q, array $registry, ?string $defaultLongCode): array {
    $phone   = preg_replace('/\D/', '', $q['phoneno'] ?? '');
    $content = (string)($q['content'] ?? '');
    $keyword = strtoupper(trim((string)($q['keyword'] ?? '')));
    $code    = preg_replace('/\D/', '', (string)($q['phonecode'] ?? ''));

    if ($code === '' && $defaultLongCode !== null) {
        $code = $defaultLongCode;               // only safe with exactly one long code
    }
    if ($code !== '' && isset($registry[$code][$keyword])) {
        return ['sender_key' => $registry[$code][$keyword], 'phone' => $phone, 'content' => $content];
    }
    foreach ($registry as $lc => $byKeyword) {  // keyword-only fallback
        if ($keyword !== '' && isset($byKeyword[$keyword])) {
            return ['sender_key' => $byKeyword[$keyword], 'phone' => $phone, 'content' => $content];
        }
    }
    return ['sender_key' => null, 'phone' => $phone, 'content' => $content, 'review' => true];
}

$result = route_inbound($_GET, $registry, count($registry) === 1 ? array_key_first($registry) : null);
http_response_code(200);

The trap: phonecode and keyword are optional in the push, so a router that assumes both are present drops real replies. Unmatched messages go to a review queue, and the handler still returns 200 so the push is acknowledged.


Renaming, Retiring and Replacing a Sender

SMS sender ID. Change the header on the DLT portal first. Then call SMSApi/senderid/update with the sId as id and the new senderid, re-read the list, and check the new name and its isEnabled state. Templates are registered against headers on DLT, so update the pairing on DLT and re-sync the sender_template table before sending with the new name. To retire, close the registry row with valid_to, stop routing to it, and only then call delete. The knowledge base entry on deleting a sender ID by mistake notes that a deleted sender cannot be used until it is added again and that past messages are unaffected.

WhatsApp number. A number change is an onboarding event, not an API call. Templates are scoped per number, so a new number starts with its own template set; plan template approval for the new number before cutting traffic over, and run both numbers in parallel until the old one’s report window has closed.

RCS bot. A new bot gets a new botId and appears in botsList once active. Templates are listed per bot, so the same template-approval lead time applies. Keep the old bot’s row open in the registry until its delivery rows stop arriving.

Telegram bot. A save with a new token swaps the account’s bot immediately. Record the old and new handles with validity dates so history stays attributable, and tell subscribers: Telegram chats belong to a bot, so users who started the old bot have not started the new one.


Ten Mistakes That Break Sender Attribution

  1. Comparing a WhatsApp number as a number on one side and a string on the other. Normalise to a digit string at the parser boundary.
  2. Treating isEnabled as a boolean. It holds a status string such as "Pending". A truthy check marks a pending sender as enabled.
  3. Reading count on the sender list as an integer. It is an object with total, pending, active and rejected.
  4. Resolving RCS delivery rows through a live bot lookup. Inactive bots drop out of botsList. Resolve from a snapshot.
  5. Sending with a sender ID that is not paired with the template. The message fails with SENDERID_MISMATCH. Check the pair before the send.
  6. Deleting a registry row when a sender is retired. Late delivery rows then cannot be attributed. Close it with a validity date instead.
  7. Letting sandbox bots into production routing. Filter on isSandbox, not on a naming convention.
  8. Looping WhatsApp report queries over a hard-coded number list. A number missing from the list means its traffic never reaches your reports. Cross-check with analytics grouped by waNumber.
  9. Assuming phonecode and keyword are always present on inbound SMS. Both are optional in the push. Route with fallbacks and a review queue.
  10. Rotating the Telegram token without recording it. The token is write-only and the rows carry no bot field, so the registry is the only record of which bot sent what, when.

Decision Matrix

If you need to…SMSWhatsAppRCSTelegram
Add a new sender from your own codeSMSApi/senderid/create, after DLT registrationNot available; onboardingNot available; provisioningrest/tg/v1/setup save
Know a sender is usableisEnabled indicates activeNumber is onboarded and templates approvedPresent in botsList with the right isSandboxSetup get returns your bot
Run two brands side by sideTwo headersTwo numbersTwo botsTwo accounts
Attribute a delivery rowsenderNamewabaNumberbotName via snapshotThe account’s bot
Attribute an inbox rowphonecode plus keywordwabaNumberbotIdThe account’s bot
List all sendersSMSApi/senderid/readAnalytics groupBy=waNumber plus your configurationrest/rcs/v1/bots (active only)Setup get
Test safelySandbox accountTest numbers you controlA bot with isSandbox of 1A test account’s bot
Change a sender’s nameDLT first, then updateNew numberNew botsave with new details

For how senders feed into the wider multi-channel picture, including which channel to fall back to when a sender cannot reach a recipient, see multi-channel fallback.


Pre-Launch Checklist

Registry

  • Every sender has a surrogate sender_key and a brand.
  • Every channel identifier is stored as text, normalised (uppercase header, digit-only number).
  • Senders are retired with valid_to, never deleted.
  • Outbound rows record sender_key at submit time.

SMS

  • Sender IDs are validated as one to six uppercase letters or digits before create.
  • sId is captured by reading the list after every create.
  • isEnabled is compared against an allowlist of active values, never tested for truthiness.
  • Every send checks that senderid is in the template’s senderIds.

WhatsApp

  • wabaNumber and waNumber are normalised to digit strings before comparison.
  • Report bodies are parsed with numeric identifiers quoted first.
  • Report jobs loop over every number, and the list is cross-checked against analytics grouped by waNumber.

RCS

  • The bot list is snapshotted daily, including name, botId and isSandbox.
  • Delivery rows resolve botName through the snapshot with a time window.
  • Sandbox bots are excluded from production routing.

Telegram

  • The bot token is written to your secrets manager when saved.
  • Bot handle changes are recorded with dates.

Inbound SMS

  • Keywords are uppercased and trimmed before routing.
  • Missing phonecode or keyword falls back gracefully to a review queue.
  • The handler returns 200 after enqueueing, whatever the routing result.

Operations

  • Unresolved report rows raise an alert and are backfilled when the registry changes.
  • Every sender list response is stored raw for a rolling window.

Unspecified Behaviour and How to Code Around It

The behaviours below are not pinned down by anything you can read ahead of time. Each item gives the choice that stays safe whichever way the behaviour turns out, and none of them require you to wait for an answer.

One. Treat only observed “active” values of isEnabled as usable. The sample shows "Pending", and the count buckets suggest active and rejected states. Keep an allowlist populated from senders that have actually delivered, and treat every other value as not usable. An unknown value then fails closed: a delay, not a rejected campaign.

Two. Pass the sId as the id for update and delete, then re-read. The update and delete calls take an id, and sId is the only identifier the list returns. Confirm the change by re-reading and checking the row with that sId. The confirmation read keeps the operation correct however the identifier is resolved internally.

Three. Enforce the six-character rule even if the platform accepts more. The create page describes Indian sender IDs as up to six alphanumeric characters. A six-character validator never produces a header that some stricter check will reject, and it costs nothing if the limit is looser elsewhere.

Four. Split senderIds on commas even when you have only seen one value. The field name is plural and a template can map to several headers. A split that finds one value returns a one-element set; a split that finds several returns all of them. Either way the pair check is correct.

Five. Snapshot the RCS bot list before you need history. Deactivated bots drop out of the live list, and delivery rows name bots rather than numbering them. A daily snapshot of botId, name and isSandbox turns “which bot was this?” into a join.

Six. Resolve RCS names within a time window. Whether a bot name can be reused after a bot is replaced is not something to rely on either way. Match botName against snapshot rows whose validity covers the delivery timestamp, and ambiguity cannot arise.

Seven. Quote WhatsApp numeric identifiers before parsing, even if they fit today. A twelve-digit number is safe in a double, a nineteen-digit uuId is not, and a future field on the same row might be either. Quoting every digit run for known identifier keys before JSON.parse is correct whatever the lengths turn out to be.

Eight. Build the WhatsApp number list from two sources. Keep numbers in configuration and also derive them from analytics grouped by waNumber. Any number that appears in one source but not the other raises an alert. Two independent sources catch an omission that either alone would hide.

Nine. Route inbound SMS without assuming phonecode is present. With one long code, default to it; with several, route on keyword and queue the rest for review. The router is correct whether the platform always sends phonecode or only sometimes does.

Ten. Treat a Telegram save as irreversible for reads. The token cannot be read back, so capture it in your secrets manager in the same transaction that calls save, and record the handle and date in the registry. You then never need the platform to tell you which token is live.

Eleven. Close registry rows instead of deleting platform senders first. Delivery rows can arrive after a sender is removed. Set valid_to in your registry, wait for the report window to pass, then call the platform delete. Attribution survives however long late rows keep arriving.

Twelve. Store raw sender list responses for a rolling window. When a send fails with a sender-related cause, the stored list shows exactly what state the sender was in at the time. A cheap daily blob replaces a guess with a lookup.


FAQs

What is a sender ID on SMSGatewayCenter?
It is the header shown as the sender of an SMS, such as SMSGAT. In India it must be registered on an operator DLT portal first; the platform record is then managed through SMSApi/senderid/create, read, update and delete.

How do I get the ID of a sender I just created?
The create response returns no identifier. Call SMSApi/senderid/read, find the row whose senderName matches, and store its sId as text.

Is isEnabled true or false?
Neither. It is a status string; the published sample value is "Pending". Compare it against known values rather than testing it as a boolean.

Why does my SMS fail with SENDERID_MISMATCH?
The sender ID you used is not the one registered with the template. Check the template’s senderIds and send with a header that appears there.

Can one template be used with several sender IDs?
Yes, if each sender ID is mapped to the template on DLT and in your account.

What identifies the sender on WhatsApp?
The WhatsApp Business number, passed as wabaNumber. It is required on send, template and media writes, and report queries.

Why does wabaNumber come back as a number on reports and a string on the inbox?
The two endpoints serialise it differently, and analytics renames it waNumber. Normalise every occurrence to a digit string before comparing.

How do I list my RCS bots?
Call GET rest/rcs/v1/bots with output=json. Only active bots are returned, and isSandbox of 1 marks a sandbox bot.

How do I know which RCS bot a delivery row came from?
The delivery row carries botName. Map it to a botId through a stored snapshot of the bot list, because inactive bots are not returned by the live list.

Can I run several Telegram bots on one account?
No. The account holds one bot, configured through rest/tg/v1/setup. Separate Telegram identities need separate accounts.

Can I read my Telegram bot token back?
No. It is write-only. Store it in your own secrets manager when you save it.

How do I tell which campaign an inbound SMS belongs to?
Route on the long code (phonecode) and keyword from the push. Both are optional, so fall back to keyword-only routing and a review queue.

Should I delete a sender ID I no longer use?
Close it in your own registry first, wait until its delivery reports have stopped arriving, then delete it on the platform. A deleted sender cannot be used until it is added again.

Where should the sender be recorded for attribution?
On your outbound message row at submit time. Report fields are the fallback, not the primary source.

If you are planning a multi-brand or multi-channel rollout and want a second pair of eyes on the sender design, talk to the SMSGatewayCenter team.


Every message traced to the brand that sent it

Build your sender registry against the SMSGatewayCenter developer API and test create, sync and resolution in the sandbox before live traffic.


Recent Articles


Save this interesting page on your favorite Social Media

Blog Author logo

SMS Gateway Center Desk

SMS Gateway Center is one of the largest and leading SMS Provider in India. It is run by a large professional team to cater small companies to large corporate companies. SMS Gateway Center is associated with the best operators in India covering the entire states in India. SMS Gateway Center has been serving through its SMS Resellers in more than 20 states in India. To become our SMS Reseller, kindly contact us

Looking for the best business communication solutions, get in touch!