SMSGatewayCenter Blog

Multi-Brand Messaging Architecture: Accounts, Sub-Users and Reseller Child Accounts for SMS, WhatsApp, RCS and Telegram

Running several brands through one messaging platform is an account-boundary decision before it is a code decision. This guide maps what each brand owns on SMS, WhatsApp, RCS and Telegram, compares a single account, team sub-users, reseller child accounts and customer-owned accounts, and walks through the eight reseller user endpoints with the traps that break provisioning and credit transfers.

Featured image for Multi-Brand Messaging Architecture: Accounts, Sub-Users and Reseller Child Accounts for SMS, WhatsApp, RCS and Telegram
Three tinted geometric platforms connected to one shared base, representing several brands sharing one messaging platform
Several brands, one platform: the account boundary decides what each brand shares and what it owns.

Table of Contents

  1. The Short Answer
  2. TL;DR
  3. Four Tenancy Models, Not One
  4. What a Brand Owns on Each Channel
  5. Six Things the Account Boundary Decides
  6. Decision Matrix: Where Each Brand Should Live
  7. The Brand Registry Schema
  8. Resolving Credentials Per Brand at Send Time
  9. The Reseller User API at a Glance
  10. Creating a Child Account and Reading It Back
  11. The User Profile Row, Field by Field
  12. Epoch Milliseconds and the Midnight IST Expiry
  13. Moving Credits Without Double Spending
  14. Reconciling Transfers with the User Credit History
  15. Sender IDs, DLT Entities and Templates Per Brand
  16. WhatsApp Numbers, RCS Brands and Telegram Bots
  17. One Webhook Per Account: Routing Delivery Reports
  18. Passwords and Credential Rotation for Child Accounts
  19. Four Code Samples, Four Traps
  20. Migration Checklist
  21. Unspecified Behaviour and How to Code Around It
  22. FAQs

The Short Answer

When one system sends messages for several brands, the first decision is not the schema or the queue. It is where the account boundary goes, because on SMSGatewayCenter a few things exist exactly once per account: the SMS wallet, the SMS delivery report webhook and the Telegram bot. Everything that must be isolated per brand along one of those three axes (a separate budget, a separate delivery callback, a separate Telegram identity) needs its own account. Everything else (sender IDs, DLT templates, WhatsApp numbers, RCS brands and bots) can live side by side inside one account and be selected per request.

That gives four workable tenancy models:

  1. One account, many brands. Cheapest to run. Brands are rows in your own registry, selected by sender ID, WhatsApp number or RCS bot on every call.
  2. Team sub-users under one direct account. Separate logins for people, not separate tenants. Sub-users carry the main account’s products and payment type, cannot create further sub-users, and inherit pricing from the main account.
  3. Reseller child accounts. Real, separate accounts created and funded through eight SMSApi/reseller/ endpoints. Each child has its own balance, its own RCS brand and bots, and its own login.
  4. Customer-owned accounts connected through OAuth. For SaaS products where each customer should own the sending relationship.

For most engineering teams the right answer is model 1 for brands that share a legal entity and a budget, and model 3 for brands that need a hard budget wall, their own Telegram bot or their own delivery callback. The rest of this guide shows how to decide, how to model it, and how to drive the reseller API without double-crediting an account on a retry.

TL;DR

  • Three things are per account: the SMS wallet (billing fires at submission), the SMS delivery report webhook, and the Telegram bot. Isolating any of them per brand means separate accounts.
  • Several things are per request: sender ID, dltEntityId and dltTemplateId on SMS, wabaNumber on WhatsApp reports, RCS bot selection through templates. One account can carry many brands on these axes.
  • Team sub-users are logins, not tenants. They inherit products, payment type and pricing; SMSApi/rateplan/read answers a sub-user with error 486, “Rate plans for sub-users are undefined.”
  • Reseller child accounts are tenants. Eight endpoints under SMSApi/reseller/: readuser, createuser, updateuser, generateuserpassword, resetuserpassword, readcredithistory, addcredit, removecredit. Every response carries "api": "user", not "reseller".
  • createuser returns no identifier. Read the new account back with readuser by userloginname and store the quoted userId it returns.
  • Profile timestamps are epoch milliseconds in quoted strings. The sample expDate of "1709231400000" is exactly midnight IST on 1 March 2024; formatted in UTC it shows 29 February.
  • Credit transfers are not idempotent and return no balance or transaction id. Put a unique reference in comment, and on any timeout read readcredithistory for the child before resending.
  • Credit products are SMS | Voice | Miss Call. WhatsApp and RCS spend per brand has to be tracked in your own ledger.
  • Parameter spellings differ between tables and samples (mobileno and mobileNo, transactiontype and type, adjustments and adjustment). Send both key spellings with the same value and verify by read-back.
  • Always use a form encoder. Hand-built bodies are how &region turns into an HTML entity and how &=product=SMS sends a parameter with no name.

Four Tenancy Models, Not One

“Multi-brand” covers very different situations. A retail group with three store brands under one legal entity, a fintech that white-labels its OTP flow for partner banks, and a marketing agency sending for forty clients all have “several brands”, but they need different boundaries. The platform gives you four places to draw that boundary.

Four-column comparison of tenancy models: one account with many brands, team sub-users, reseller child accounts and customer-owned accounts via OAuth, showing what each shares and isolates
The four tenancy models side by side. The wallet, the SMS webhook and the Telegram bot are the three things that force a separate account.

Model 1: one account, many brands

Every brand’s sender IDs, DLT templates, WhatsApp numbers and RCS bots are registered in a single account. Your application chooses the brand on every request by passing the right senderid, dltEntityId, dltTemplateId, wabaNumber or RCS template code. There is one set of credentials, one wallet and one SMS webhook.

This model is right when the brands share a legal entity, a budget owner and an operations team. It is the cheapest to run and the easiest to report on, because every delivery report and every credit movement lands in one place. It is wrong the moment one brand’s campaign must not be able to drain another brand’s balance, or two brands each need their own Telegram bot.

Model 2: team sub-users under a direct account

Sub-users are separate logins under a main direct-customer account, created in the panel under Sub-Users > Create. The platform generates the password and emails it to the sub-user. According to the sub-users knowledge base entry, a sub-user gets “the same products, payment type and login-verification (OTP) methods as your main account” and has “no ability to create further sub-users”. The feature has to be enabled on the account, and it is available on direct customer accounts, not on reseller accounts or on sub-users themselves.

The rate plan endpoint confirms the pricing side: sub-users are userType = 4, “cannot access rate plans”, and “inherit pricing from the main account”. A call to SMSApi/rateplan/read from a sub-user returns code 486 with the message “Rate plans for sub-users are undefined. Please review the rates in the main account”.

Treat sub-users as an access-control feature for people. They let a campaign manager for Brand A sign in without the main password, and they let you deactivate that login when the person leaves. They are not a budget wall or a data partition you should rely on in code.

Model 3: reseller child accounts

A reseller account can create full user accounts beneath it with SMSApi/reseller/createuser, choosing usertype of customer or reseller. Each child is its own account: its own login, its own smsBalance, its own status and expiry date, and on RCS its own brand, bots and templates. The reseller funds children by transferring credits with SMSApi/reseller/addcredit and claws them back with SMSApi/reseller/removecredit.

This is the model for hard isolation. A child account cannot spend what it was not given, it has its own webhook, and it can register its own Telegram bot. The cost is operational: every child is another set of credentials to store, another balance to monitor and another place to reconcile.

Model 4: customer-owned accounts through OAuth

If you are building a product that other businesses use to send their own messages, the cleanest boundary may be that each customer owns their SMSGatewayCenter account and connects it to your product. OAuth for messaging platforms covers that flow end to end: authorization code with PKCE S256, a 10-minute code, a 3600-second access token and a 30-day refresh token. In this model you never hold the customer’s balance, and DLT, sender ID and template registration stay with the business that owns them.


What a Brand Owns on Each Channel

Before choosing a model, list what “a brand” means on each channel. On this platform the answer is different for every channel, and the names do not line up. Sender identity across SMS, WhatsApp, RCS and Telegram covers the field names in depth; the summary that matters for tenancy is this:

ChannelBrand identity unitCardinality per accountSelected per request byRegistered where
SMS (India)Sender ID (header) tied to a DLT principal entity and templatesMany sender IDs; SMSApi/senderid/read lists themsenderid, plus optional dltEntityId and dltTemplateId on SMSApi/sendDLT portal, then the account
WhatsAppWABA phone numberSeveral numbers addressable; WAApi/report requires wabaNumber and analytics can group by waNumberThe number used on the send and the wabaNumber on readsMeta onboarding through the account
RCSRCS brand, with one or more bots linked to itSeveral brands and bots; every bot must be linked to a brandThe template, whose code belongs to a botPanel: RCS > My RCS Brands, then bots
TelegramOne botExactly one per accountImplicit: the account’s botrest/tg/v1/setup with botName, botHandle, botToken
Inbound SMSKeyword on a long code or short codeMany keywordsKeyword in the inbound pushPanel keyword setup

Three consequences fall out of that table.

SMS brands are cheap to co-host. Several sender IDs, each mapped to its own templates, is the normal state of an Indian SMS account. The SENDERID_MISMATCH outcome is what you get when a brand’s sender ID is paired with another brand’s template, so the pairing must come from one registry row, never from two independent lookups.

RCS has a real brand object. The RCS brand stores business identity: brand name, official website, industry, logo, contact person and address, and carriers use these details to verify the sender. If your brands are distinct businesses to a carrier, they are distinct RCS brands, even inside one account.

Telegram forces the split. One bot per account means two Telegram identities need two accounts. If only one of your brands uses Telegram, that brand can still live in a shared account; if two do, at least one of them needs a child account.


Six Things the Account Boundary Decides

Everything in a multi-brand design either follows the account or follows the request. The six below follow the account, and each one is a reason to split.

1. The wallet

SMS billing fires at submission: the pricing terms state that “the applicable per-SMS rate will be deducted from your wallet while sending SMS” and that “credits are non-refundable once SMS is successfully submitted to the operator.” There is one wallet per account. If Brand A and Brand B share an account, a runaway campaign on Brand A spends Brand B’s money, and nothing on the platform stops it. SMSApi/account/readstatus returns one smsBalance, not one per sender ID.

You can enforce per-brand budgets in your own ledger inside a shared account, and many teams do. But that is a soft wall: it holds only if every path that sends goes through your ledger, including the panel, a colleague’s script and a scheduled campaign someone set up by hand. A child account is a hard wall.

2. The SMS delivery report webhook

There is one SMS delivery report webhook per account. Every brand in a shared account delivers its status callbacks to the same URL, and your receiver must work out which brand each report belongs to. That is solvable (see One Webhook Per Account), but if a brand’s delivery events must go to a different system owned by a different team, the clean answer is a separate account.

3. The Telegram bot

One bot per account, configured through rest/tg/v1/setup. rest/tg/v1/dlr rows carry no bot field, because there is only ever one bot to report on. See the Telegram messaging API for the full contract.

4. Pricing and the rate plan

A child account created through the reseller API has its own rate plan, which is what makes reseller margins possible. Team sub-users do not: they inherit the main account’s pricing, and the rate plan endpoint answers them with 486. If two brands must be billed at different per-message rates, they cannot both be sub-users of one account.

5. Credentials and blast radius

An API key belongs to an account. In a shared account, one leaked key can send as every brand. In a child-account layout, a leaked key for Brand A’s child sends only as Brand A and spends only Brand A’s balance. For a platform sending on behalf of clients, that difference is often the whole argument.

6. Feature enablement

Some channels are switched on per account. On RCS, the reseller knowledge base states that “RCS must be enabled on each user account before the RCS menu appears” for that user. Sub-users, by contrast, get “the same products” as the main account. If one brand should have RCS and another must not, only separate accounts express that.

Everything else (which sender ID, which template, which WhatsApp number, which RCS bot) is a per-request choice, and per-request choices belong in your brand registry, not in your account layout.


Decision Matrix: Where Each Brand Should Live

Use the strictest row that applies. If any single requirement lands in the child-account column, the brand needs a child account, even if every other requirement would be happy in a shared one.

Requirement for the brandOne account, many brandsTeam sub-userReseller child accountCustomer-owned (OAuth)
Separate SMS budget that cannot be overspentSoft wall only (your ledger)No, shared payment typeYes, own smsBalanceYes, customer’s wallet
Different per-message SMS priceNoNo, inherits main pricingYes, own rate planYes, customer’s plan
Own Telegram botOnly one brand per accountNoYesYes
Own SMS delivery webhook URLNo, one per accountNoYesYes
Own login for staffShared loginYesYesCustomer’s own
Several sender IDs and templatesYesTreat as shared with the main accountYesYes
Several WhatsApp numbersYesTreat as shared with the main accountYesYes
Own RCS brand and botsYes, several brands per accountTreat as shared with the main accountYesYes
API key compromise limited to one brandNoNoYesYes
You hold and resell creditsNot applicableNot applicableYesNo
Lowest operational overheadBestGoodWorstDepends on your product

Three common shapes come out of the matrix:

Group brands under one legal entity (a retailer with three store names, a bank with separate card and loan sender IDs): one account, many brands. Add sub-users if different people run different brands. Enforce per-brand budgets in your ledger and accept that it is a soft wall.

Agency or platform sending for clients who each have their own DLT registration and budget: a reseller account with one child account per client. The child holds the client’s sender IDs, templates and balance; your platform holds the parent credentials and the child credentials.

SaaS product where each customer is the sender of record: customer-owned accounts through OAuth. You never touch their balance, and your registry stores tokens instead of passwords.

Mixed layouts are normal. A platform might run its own marketing brands in one shared account, give each client a child account, and let its largest customers connect their own accounts. The registry in the next section handles all three with the same row shape.


The Brand Registry Schema

Whatever layout you choose, the application needs one place that answers “for brand X on channel Y, which account, which credentials and which identity do I use?” That is the brand registry. It is the multi-brand counterpart of the outbound message table: every outbound row should carry a brand_id, and every brand_id resolves through the registry.

-- One row per account you can send through, whatever tenancy model it uses.
CREATE TABLE messaging_account (
  account_id        BIGSERIAL PRIMARY KEY,
  tenancy_model     TEXT NOT NULL CHECK (tenancy_model IN
                      ('shared','subuser','child','oauth')),
  parent_account_id BIGINT REFERENCES messaging_account(account_id),
  login_name        TEXT NOT NULL,          -- userid / userloginname
  platform_user_id  TEXT,                   -- quoted userId from readuser, kept as text
  secret_ref        TEXT NOT NULL,          -- pointer into your secret store, never the key
  webhook_path_key  TEXT UNIQUE,            -- random token in this account's DLR URL
  status            TEXT NOT NULL,          -- last observed userStatus
  expires_at        TIMESTAMPTZ,            -- from expDate, see the epoch section
  last_synced_at    TIMESTAMPTZ
);

-- One row per brand. A brand lives in exactly one account per channel.
CREATE TABLE brand (
  brand_id     BIGSERIAL PRIMARY KEY,
  brand_key    TEXT UNIQUE NOT NULL,        -- 'acme-retail', stable, human readable
  legal_entity TEXT NOT NULL,
  budget_owner TEXT NOT NULL
);

-- Channel identity per brand. The pairing rules live here, nowhere else.
CREATE TABLE brand_channel (
  brand_id        BIGINT NOT NULL REFERENCES brand(brand_id),
  channel         TEXT NOT NULL CHECK (channel IN ('sms','whatsapp','rcs','telegram')),
  account_id      BIGINT NOT NULL REFERENCES messaging_account(account_id),
  sender_key      TEXT,     -- SMS senderid, WhatsApp number, RCS bot name
  dlt_entity_id   TEXT,     -- SMS only, text: long numeric
  enabled         BOOLEAN NOT NULL DEFAULT FALSE,
  PRIMARY KEY (brand_id, channel)
);

-- Templates are owned by a brand and a sender, never by a channel alone.
CREATE TABLE brand_template (
  brand_id        BIGINT NOT NULL,
  channel         TEXT NOT NULL,
  template_key    TEXT NOT NULL,            -- your name for it
  platform_ref    TEXT NOT NULL,            -- dltTemplateId, WA name+language, RCS templateCode
  sender_key      TEXT,                     -- the sender this template is registered with
  PRIMARY KEY (brand_id, channel, template_key),
  FOREIGN KEY (brand_id, channel) REFERENCES brand_channel(brand_id, channel)
);

-- A second Telegram brand on the same account is a design error. Make it impossible.
CREATE UNIQUE INDEX one_telegram_brand_per_account
  ON brand_channel(account_id) WHERE channel = 'telegram';

Five design choices in that schema matter more than the column names.

The primary key on brand_channel is (brand_id, channel). A brand lives in exactly one account per channel. If you allow a brand’s SMS to go out of two accounts, delivery reports, balances and DLT pairings split across two places and reconciliation never closes.

Templates carry a sender_key. On SMS a DLT template is registered against specific sender IDs, and one template can map to several sender IDs. Storing the sender next to the template is what lets the resolver refuse a mismatched pair before the platform does.

Secrets are references. The registry stores a pointer into a secret manager, not the API key. The registry is read on every send; the secret store is read only when a client is built.

Platform identifiers are text. userId arrives quoted ("6"), credit history uuId is sixteen digits in the sample, and SMS transaction identifiers run to eighteen or nineteen digits. Every identifier the platform hands back goes into a text column.

The partial unique index encodes the Telegram rule. The database, not a code review, stops a second brand from claiming an account’s only bot.


Resolving Credentials Per Brand at Send Time

The resolver turns (brand_key, channel, template_key) into everything one API call needs: base URL, account credentials, sender and template reference. It should be the only code in the system that knows which account a brand uses.

Four rules keep it safe.

Resolve once, pass explicitly. The resolver returns a complete, immutable send context. Downstream code never looks up “the default sender ID” or “the account’s DLT entity” on its own. Account-level defaults are exactly the thing that is wrong for the second brand in a shared account.

Send dltEntityId and dltTemplateId on every SMS. SMSApi/send accepts both as optional parameters. In a single-brand account you can often omit them; in a multi-brand account, passing them explicitly is what guarantees the brand’s own entity and template are used.

Fail closed when a brand has no identity on a channel. If Brand B has no brand_channel row for RCS, the resolver raises. It does not fall back to Brand A’s bot, and it does not fall back to the account’s first bot. A missing mapping is a configuration bug, and a message sent under the wrong brand is worse than one not sent.

Authenticate with the apikey header and still send userid. The apikey header is accepted on every endpoint. HTTP header names are case-insensitive, so the apiKey spelling in the reseller and SMSApi/ tables is the same header. Send the account’s userid alongside it. In a layout with child accounts, the resolver picks the child’s userid and key for sends and the parent’s for reseller calls, and those two credential sets must never be swapped.

A resolved context looks like this:

{
  "brand_key": "acme-retail",
  "channel": "sms",
  "account": { "login_name": "acmechild01", "secret_ref": "vault:msg/acmechild01" },
  "base_url": "https://unify.smsgateway.center/",
  "sender_key": "ACMERT",
  "dlt_entity_id": "1201159XXXXXXXXXXXX",
  "template": { "template_key": "order-shipped", "platform_ref": "1207161XXXXXXXXXXXX" }
}

The two DLT values are placeholders; real ones are long numerics and are stored as text.


The Reseller User API at a Glance

Child accounts are created and funded through eight endpoints under https://unify.smsgateway.center/SMSApi/reseller/. All of them take the parent’s credentials plus a userloginname naming the child, and all of them return the familiar response envelope with "api": "user" and a quoted "code": "200" on success.

EndpointMethod on its pagePurposeRequired parameters beyond credentialsSuccess msgReturns an identifier?
SMSApi/reseller/readuserPOST and GETRead a child’s profileuserloginname, output“success”Yes, userId inside userList[i].user
SMSApi/reseller/createuserPOST onlyCreate a child accountuserloginname, usertype, email, mobileno, fullname, address, region, expirydate, output“User created successfully.”No
SMSApi/reseller/updateuserPOST onlyUpdate a child’s detailsSame set as create“User update successfully.”No
SMSApi/reseller/generateuserpasswordPOST onlyEmail the child a reset linkuserloginname, output“Password changed successfully.”No
SMSApi/reseller/resetuserpasswordPOST onlySet the child’s password directlyuserloginname, newPassword, output“Password changed successfully.”No
SMSApi/reseller/readcredithistoryPOST and GETRead a child’s credit movementsuserloginname, fromdate, todate, output“success”Yes, uuId per history row
SMSApi/reseller/addcreditPOST and GETTransfer credits to a childuserloginname, product, transactiontype, credits, comment, output“Credit transferred successfully.”No
SMSApi/reseller/removecreditPOST and GETTake credits back from a childSame set as add“Credit removed successfully.”No

Five things in that table shape every client you write against it.

Use POST for everything. Four of the eight accept GET, including both credit writes. A GET that moves money ends up in proxy logs, browser history and link prefetchers. POST with a form-encoded body is accepted by all eight, so a single code path covers the family.

The api field says user, the path says reseller. If your client asserts that response.api matches the path segment, it will reject every successful reply. Check response.status and nothing else to decide success, the same rule that applies across the rest of the API.

Writes return no identifier. createuser does not return the new userId, and the credit writes return neither a transaction id nor the resulting balance. Every write must be confirmed by a read. The next three sections show how.

Two success messages lie about what happened. generateuserpassword sends the child an email with a reset link, yet its success msg reads “Password changed successfully.” Treat it as “reset email dispatched” and never tell an operator that the password has changed. Never parse msg at all; branch on status.

Read the child with userloginname, even for a listing. The readuser page describes retrieving “a list of all users”, but userloginname sits among the required parameters and the sample returns one row with count 1. Keep your own list of children in messaging_account and read each one by name.


Creating a Child Account and Reading It Back

Provisioning a child account for a new brand is a three-call sequence: create, read back, and verify that what landed matches what you sent.

Step 1: create

The create table and the sample on the same page spell some parameters differently:

ConceptParameter tableSample request body
Mobile numbermobileno (“Should be integer”)mobileNo=919999xxxxxx
Cityregion, described as “City”city=city name
Statenot listedregion=state name
Countrynot listedcountry=country name
Expiryexpirydate, YYYY-MM-DDexpirydate=2019-10-01
Login nameuserloginnameuserloginname=...

The table and sample agree on userloginname, usertype, email, fullname, address and expirydate. For the rest, the safe approach is to send both spellings with the same value: mobileno and mobileNo carrying the same number. A server that reads one key ignores the other, so the request is correct under either reading.

region is the harder one, because the table uses it for the city and the sample uses it for the state. Send what the sample sends (city, region as the state, country) and then verify with the read-back in step 2. The profile row returns postalCity, postalRegion and postalCountry separately, so one read tells you which input landed where.

Build the body with a form encoder. The sample strings are concatenated by hand, and a hand-built string containing &region is exactly where an HTML renderer or a careless copy turns &reg into a registered-trademark sign. A form encoder also handles spaces in fullname and address, which the sample sends unencoded.

Step 2: read back

POST https://unify.smsgateway.center/SMSApi/reseller/readuser
apikey: <parent key>
Content-Type: application/x-www-form-urlencoded

userid=<parent login>&userloginname=acmechild01&output=json

Take response.userList[0].user.userId, keep it as text, and write it into messaging_account.platform_user_id. From here on your registry knows the child by both its login name and its platform id.

Step 3: verify

Compare the read-back row against the create request field by field: userType against usertype (case-insensitively, since the sample returns "Reseller" for an input of reseller), emailId against email, mobileNo against the mobile number, postalCity, postalRegion and postalCountry against what you sent, and expDate against expirydate (after the conversion in the epoch section). A mismatch is a provisioning failure: alert, do not start sending.

If step 1 times out, do not resend it. Go straight to step 2. If readuser finds the login name, the create succeeded and you continue with step 3. If it does not, resend the create with the same userloginname. Every reseller call addresses the child by userloginname, so the name has to point at one account. If the resend is rejected, run the read-back once more before treating it as a failure: a rejection right after a timeout usually means the first create landed.


The User Profile Row, Field by Field

The readuser sample response is the most information-dense payload in the reseller family:

{
  "response": {
    "api": "user",
    "action": "readuser",
    "status": "success",
    "msg": "success",
    "code": "200",
    "count": 1,
    "userList": [
      {
        "user": {
          "userId": "6",
          "userName": "myuser",
          "userType": "Reseller",
          "emailId": "[email protected]",
          "mobileNo": "919999999999",
          "domainName": "smssssssssssss.com",
          "expDate": "1709231400000",
          "regDate": "1546740446000",
          "userStatus": "Active",
          "enableCMS": "Inactive",
          "fullName": "",
          "postalAddress": "",
          "postalCity": "Mumbai",
          "postalCountry": "India",
          "postalRegion": "Maharashtra",
          "smppEnabled": "1",
          "accountType": "TRANSACTIONAL",
          "smsBalance": "3900"
        }
      }
    ]
  }
}
FieldWire typeMeaning and handling
userIdQuoted digitsPlatform id of the child. Store as text.
userNameStringLogin name; equals userloginname.
userTypeString, capitalised"Reseller" in the sample; input values are lower case. Compare case-insensitively.
emailIdStringContact email.
mobileNoQuoted digits with country codeStore as text. Never parse as a number.
domainNameStringThe white-label domain associated with the user.
expDateQuoted epoch millisecondsAccount expiry. See the next section.
regDateQuoted epoch millisecondsRegistration time. "1546740446000" is 2019-01-06 07:37:26 IST.
userStatusString"Active" in the sample. Treat any other value as not sendable.
enableCMSString"Inactive" here.
postalCity, postalRegion, postalCountryString names"Mumbai", "Maharashtra", "India".
smppEnabledQuoted "1"Flag as a string.
accountTypeString"TRANSACTIONAL" in the sample.
smsBalanceQuoted integerThe child’s SMS credits. Parse with an integer parser that rejects decimals.

Two fields in that row share a name with fields on the account’s own profile endpoint but not their encoding. On SMSApi/account/readprofile, postalCity comes back as "2707", postalCountry as "101" and postalRegion as "22": numeric identifiers in a name-like field. On readuser the same three fields carry names. enableCMS is "1" on readprofile and "Inactive" on readuser. If one model class maps both payloads, it will either fail to parse or silently store an id where a name belongs. Give the two endpoints separate types, or at least separate mappers.

For the same reason, never compare a child’s postalCity from readuser with the parent’s postalCity from readprofile. They are different kinds of value that happen to share a key.


Epoch Milliseconds and the Midnight IST Expiry

createuser and updateuser take expirydate as a calendar date, YYYY-MM-DD. readuser returns expDate as epoch milliseconds in a quoted string. The sample value is worth converting by hand:

ValueAs UTCAs IST (UTC+05:30)
expDate "1709231400000"2024-02-29 18:30:002024-03-01 00:00:00
regDate "1546740446000"2019-01-06 02:07:262019-01-06 07:37:26
credit history timestamp "1570868345603"2019-10-12 08:19:05.6032019-10-12 13:49:05.603

The expiry lands on exactly midnight in India. That strongly suggests the platform turns an input date into the start of that day in IST. The practical consequences:

Formatting in UTC shows the wrong day. A dashboard running in a UTC container that prints expDate as a date shows 29 February for an account you set to expire on 1 March. An operator who “corrects” it by setting 2 March has now given the account an extra day.

Round-trip checks must convert in IST. In the verify step, convert expDate to a date in Asia/Kolkata before comparing it with the expirydate you sent.

Decide what “expires on 1 March” means for your brand. If expiry is the start of that day in IST, the account stops at midnight going into 1 March, not at the end of it. If a contract says “valid through 1 March”, send 2 March.

Parse the string, then the number. The values are quoted. In JavaScript, Number("1709231400000") is exact because thirteen-digit millisecond values are far below 2^53, but code that feeds the raw string into a date constructor gets Invalid Date. Convert explicitly.

The same convention applies to every timestamp in the reseller family, including the credit history rows in the next two sections.


Moving Credits Without Double Spending

addcredit and removecredit move money, return only a success message, and carry no idempotency key. A retry after a timeout can credit a child twice, and nothing in the response tells you it happened. This section is the fix.

Three-step credit transfer flow: record intent with a unique reference, POST addcredit with the reference in comment, then confirm by reading the child's credit history before any retry
A credit transfer is confirmed by reading it back, never by the success message. On a timeout, read first and resend only if the reference is absent.

The parameters, and where the table and sample disagree

ConceptParameter tableSample request body
Child accountuserloginnameuserloginname=YourUserLoginname
Productproduct: SMS, Voice or Miss Callproduct=SMS (on the add page, preceded by a stray &=)
Transaction typetransactiontype: purchase or adjustmentstype=purchase on add, type=adjustment on remove
Amountcredits, integercredits=10000
Notecommentcomment=...

Apply the same rule as on create: send transactiontype and type with the same value, so the request is right whichever key the server reads. The value spelling (adjustments or adjustment) cannot be hedged the same way, because you can only send one value per key. For top-ups, use purchase, where the table and both samples agree. Before your first adjustment in production, move one credit with the table spelling, read the child’s history, and check that the row’s method reflects an adjustment; pin whichever spelling produced it.

The stray &= in the add sample is a reminder to never build these bodies by concatenation. A form encoder never emits a parameter with an empty name.

The product list decides what you can allocate

product accepts SMS, Voice and Miss Call. WhatsApp and RCS are not in that list, even though the page intro mentions WhatsApp. For a multi-brand platform this means:

  • SMS budgets per child can be enforced by the platform, through transfers.
  • WhatsApp and RCS spend per brand has to be measured from delivery and analytics data and enforced in your own ledger. Reconciling a messaging invoice across four channels covers where per-message cost appears on each channel.
  • Rate plans for a child’s RCS traffic are confirmed with the platform when RCS is enabled for that user, per the reseller RCS knowledge base.

The three-step transfer

Step 1. Record intent before calling. Write a row to your own credit_transfer table with a fresh reference such as ct-7f3a9c21, the child, product, type, amount and state PENDING. Commit before the HTTP call. If the process dies mid-call, the row survives and tells the recovery job what to look for.

Step 2. POST the transfer with the reference in comment. Put the reference at the start of the comment, for example comment=ct-7f3a9c21 monthly top-up acme-retail. The comment is the only free-text field that travels with the transfer, and credit history rows return a comments field.

Step 3. Confirm by reading the child’s credit history. Whether the POST returned success, failed or timed out, call readcredithistory for the child over a window covering the call, and look for a row whose comments contains the reference. If it is there, mark the transfer CONFIRMED and store the row’s uuId and balance. If it is absent and the POST clearly failed, mark it FAILED. If it is absent after a timeout, wait and read again before concluding anything.

Only one path leads to a resend: the reference is still absent after a second read, well after the call. Resend with the same reference. If both attempts eventually land, the two rows share one reference, which makes the duplicate visible and reversible with a single removecredit.

State machine

StateEntered whenNext action
PENDINGIntent row writtenPOST the transfer
SENTPOST returned status: successRead history, match reference
UNKNOWNPOST timed out or the connection droppedWait, then read history; never resend from here directly
CONFIRMEDHistory row with the reference foundStore uuId, balance; done
FAILEDPOST returned status: error and history shows no rowAlert; safe to create a new intent
DUPLICATETwo history rows carry the same referenceReverse one with removecredit, reference it in the comment

This is the same idempotency discipline as preventing duplicate sends, applied to money instead of messages: the client owns the key, because the server does not.


Reconciling Transfers with the User Credit History

SMSApi/reseller/readcredithistory is the read side of every transfer. Unlike the account’s own SMSApi/account/readcredithistory, which has no date range, the reseller version requires fromdate and todate in YYYY-MM-DD.

{
  "response": {
    "api": "user",
    "action": "readcredithistory",
    "status": "success",
    "msg": "success",
    "code": "200",
    "count": 1,
    "historyList": [
      {
        "history": {
          "uuId": "3555847512901782",
          "type": "CREDIT",
          "method": "purchase",
          "balance": "3800",
          "amount": "100",
          "comments": "testing",
          "timestamp": "1570868345603"
        }
      }
    ]
  }
}
FieldWire typeHandling
uuIdQuoted, sixteen digits in the sampleThe history row id. Store as text; sibling ids elsewhere in the API run to nineteen digits.
typeString"CREDIT" in the sample. Direction of the movement.
methodString"purchase" here; mirrors the transaction type you sent.
balanceQuoted integerBalance after the movement.
amountQuoted integerCredits moved.
commentsStringYour comment. The reference lives here.
timestampQuoted epoch millisecondsConvert in IST as in the previous section.

The reseller history’s numbers are quoted strings; the account’s own credit history returns integer credits. A shared parser for “credit history rows” across the two endpoints must accept both, or better, the two endpoints get two row types.

Daily reconciliation per child

Once a day, for each child account:

  1. Read readcredithistory for yesterday’s IST date, both bounds the same day.
  2. Match every row with a reference in comments against your credit_transfer table. Rows with no matching reference are transfers someone made outside your system, most often in the panel. Record them, attribute them, and alert if they are unexpected.
  3. Check continuity: consecutive rows should chain, each balance equal to the previous row’s balance plus or minus amount. A gap means sending consumed credits between movements, which is expected; a jump in the opposite direction is not.
  4. Read readuser and store smsBalance as the day’s closing snapshot. Keep it as history; the platform gives you the current balance, not yesterday’s.

Store the snapshots from the first day. A daily row costs almost nothing and turns “what was Brand A’s balance on the 14th?” from an unanswerable question into a query.

Checking the parent side

The success message for addcredit reads “Credit transferred successfully”, which describes a movement from the parent to the child. Read the parent’s own balance with SMSApi/account/readstatus before and after a batch of transfers and check that the parent’s smsBalance fell by the sum you moved. If it did not, your model of where the credits come from is wrong, and you want to learn that in week one, not at month end.


Sender IDs, DLT Entities and Templates Per Brand

On Indian SMS, a brand is a chain: a principal entity registered on DLT, one or more headers (sender IDs) registered to that entity, and content templates registered against those headers. The platform account holds the sender IDs and templates; your registry holds the chain.

In a shared account

Several brands’ sender IDs sit side by side in SMSApi/senderid/read, and several brands’ templates sit side by side in SMSApi/template/read. Nothing in either list says which brand a row belongs to. The sender list returns sId, senderName, isEnabled and addTime; the template list returns mtId, identifier, template, dltTemplateId, senderIds and status. The brand is your concept, so the mapping lives in brand_channel and brand_template.

Three practices keep shared accounts clean:

  • Name templates with a brand prefix. Use identifier values such as acme-retail.order-shipped. When someone reads the template list in the panel, the brand is obvious, and your sync job can flag rows with no known prefix.
  • Validate the sender and template pair before sending. The resolver checks that the chosen sender appears in the template’s senderIds. That catches in your code what the platform would report as SENDERID_MISMATCH after the fact.
  • Pass dltEntityId and dltTemplateId on every send. Brands under different legal entities have different principal entity ids. Passing both explicitly means no send depends on an account-level default that is only right for one of the brands.

Message template management across four channels covers the template lifecycle, typed DLT variable tags and read-back rules on every channel.

In a child account

The child holds its own sender IDs and templates, and its lists contain only its own brand. That is the main operational win of child accounts on SMS: a sync job reading the child’s lists can treat every row as belonging to the brand.

A reseller can add sender names on behalf of a user in the white-label panel, under “User Sender Names”, per the reseller sender names knowledge base entry. Through the API, sender IDs and templates are managed with the SMSApi/senderid/ and SMSApi/template/ endpoints called with the child’s own credentials. Your provisioning job therefore switches credential sets partway through: parent credentials for createuser and addcredit, child credentials for sender ID and template setup.

Moving a brand between accounts

Sender IDs and templates are DLT registrations first and platform rows second. Moving a brand from a shared account to a child account means re-adding its sender IDs and templates in the child, then switching the registry row. Keep the old account’s rows until delivery reports for traffic sent before the switch have settled, because those reports still arrive through the old account.


WhatsApp Numbers, RCS Brands and Telegram Bots

WhatsApp

The WhatsApp surface is built to address more than one number per account. WAApi/report requires wabaNumber, and rest/wa/v1/analytics accepts groupBy=waNumber. In a shared account, each brand gets its own WhatsApp number, and your registry maps brand to number.

Two traps apply specifically to multi-brand WhatsApp.

The number changes type between endpoints. Report rows carry wabaNumber unquoted, analytics delivery rows carry waNumber quoted, and inbox rows carry a quoted wabaNumber. A brand lookup keyed on “the number” must normalise all three to one text form, digits only with country code, before comparing.

Media writes are number-scoped; media reads are account-scoped. Brand A’s uploaded media is visible when you list media for the account. Tag media in your own store with the brand, and never pick “the latest uploaded image” from the account list.

Template names are lower-cased and deleted by name and language, so prefix WhatsApp template names with the brand too. In a shared account, acme_order_shipped and zenith_order_shipped cannot collide.

RCS

RCS has a first-class brand object, and every bot must be linked to one. In a shared account you register one RCS brand per real business and one or more bots under each. Delivery rows from rest/rcs/v1/dlr carry botName, and inbox rows from rest/rcs/v1/inbox carry an unquoted botId, so routing an RCS event to a brand needs both mappings in your registry: bot name to brand and bot id to brand.

rest/rcs/v1/bots lists active bots only. A bot that is deactivated disappears from the list while its historical delivery rows still carry its name. Keep bots in your registry after they leave the platform list, marked inactive, so old events still resolve to a brand.

For child accounts, the reseller RCS knowledge base is explicit: each user creates their own brand, bot and templates, and RCS is enabled per user account. A reseller can share selected templates with sandbox users for testing and view per-user RCS reports. See the reseller RCS features entry.

Telegram

One bot per account, set with rest/tg/v1/setup. botToken is write-only: the setup read returns botName and botHandle, never the token. For a multi-brand layout:

  • One Telegram brand per account, enforced by the partial unique index in the registry.
  • A second Telegram brand means a second account. With a reseller parent, that is a child account; without one, it is a separate direct account.
  • Store each bot’s token in your secret store keyed by account, because you cannot read it back from the platform if you lose it.

One Webhook Per Account: Routing Delivery Reports

Every account has one SMS delivery report webhook. In a shared account, reports for every brand arrive at one URL. In a child-account layout, each child has its own webhook, which you can point at a URL that identifies the child.

Give every account its own URL path

Even when all accounts deliver to the same service, register each with a distinct path containing a random token:

https://hooks.example.com/sms-dlr/k7Qm2xV9pLr4/   <- shared account
https://hooks.example.com/sms-dlr/Zp8wT3nB6yHc/   <- child for acme-retail
https://hooks.example.com/sms-dlr/Rf5jK1sD0qMu/   <- child for zenith-bank

The token is messaging_account.webhook_path_key. The receiver resolves the account from the path before it parses the body, so a report can never be attributed to the wrong account. A random token also means a stranger who learns one URL cannot guess another.

Resolve the brand from your own outbound row

Within an account, resolve the brand by joining the report to your outbound message row by transaction id. Your outbound row already carries brand_id. Do not resolve the brand from the sender name in the report: in a shared account two brands could legitimately share a header during a migration, and the join on transaction id is exact where the sender name is not.

Persist raw, then reconcile

The push payload’s field list is not something to build a strict parser around. Store the raw body with the resolved account and a receive timestamp, return quickly, and let a worker extract the transaction id. Then reconcile against SMSApi/reports/status with method=getDlr, which returns a stable row shape including uuId, mobileNo, submitTime and status. Delivery report ingestion as a system of record walks through that pipeline.

The other channels poll

WhatsApp, RCS and Telegram inbound traffic is read by polling rest/wa/v1/inbox, rest/rcs/v1/inbox and rest/tg/v1/inbox, and their delivery data by polling their report and dlr endpoints. Polling is per account and per credential set, so a layout with ten child accounts runs ten pollers per channel. Schedule them with jitter and a shared rate budget, not ten cron lines at the same minute. Receiving messages on four channels has the inbox contracts.


Passwords and Credential Rotation for Child Accounts

The reseller family offers two ways to deal with a child’s password.

generateuserpasswordresetuserpassword
What happensThe child receives an email with a reset linkYou set the new password directly
Extra parameterNonenewPassword
Who knows the new passwordOnly the childYou, and anything that logged the request
Success msg“Password changed successfully.”“Password changed successfully.”
Right forA human who signs in to the panelA service account you operate entirely

For children that a client’s staff use, prefer generateuserpassword. You never hold the password, and the email goes to the address on the child’s profile. Read the profile first and confirm emailId is the address you expect; a reset link sent to a stale address is an account handover.

For service children your platform operates, prefer API keys over passwords. A password is needed for panel access; your sending code should authenticate with the child’s API key in the apikey header plus userid. If you do set a password with resetuserpassword, generate it, write it straight to the secret store, and make sure the request body is excluded from every log, trace and error report. The body contains newPassword in clear text.

Rotate keys per account, never globally. In a child-account layout, rotating Brand A’s key touches one registry row and one secret. That is the blast-radius benefit of child accounts, and it only holds if keys are never shared across children.

For team sub-users, password resets are done from Sub-Users > Manage in the panel, alongside activate, deactivate and email and name updates.


Four Code Samples, Four Traps

Each sample shows one trap from this guide. They share the client shape used across the API: parse loosely, read only the top-level status, return on failure, and bind typed fields only on success.

Python: reading a child account without mis-dating its expiry

The trap: quoted epoch milliseconds formatted in UTC show the day before the expiry you set.

import requests
from dataclasses import dataclass
from datetime import datetime, timezone, timedelta

BASE = "https://unify.smsgateway.center/SMSApi/reseller/"
IST = timezone(timedelta(hours=5, minutes=30))

@dataclass(frozen=True)
class ChildAccount:
    user_id: str          # quoted digits, kept as text
    login: str
    user_type: str        # normalised to lower case
    status: str
    expires_ist: datetime
    sms_balance: int
    postal_city: str      # a NAME on readuser, unlike readprofile

def epoch_ms_ist(value: str) -> datetime:
    return datetime.fromtimestamp(int(value) / 1000, tz=IST)

def read_child(parent_login: str, api_key: str, child_login: str) -> ChildAccount | None:
    r = requests.post(
        BASE + "readuser",
        headers={"apikey": api_key},
        data={"userid": parent_login, "userloginname": child_login, "output": "json"},
        timeout=15,
    )
    body = r.json().get("response", {})
    if body.get("status") != "success":
        return None
    rows = body.get("userList") or []
    if not rows:
        return None
    u = rows[0].get("user", {})
    return ChildAccount(
        user_id=str(u["userId"]),
        login=u["userName"],
        user_type=u.get("userType", "").lower(),
        status=u.get("userStatus", ""),
        expires_ist=epoch_ms_ist(u["expDate"]),
        sms_balance=int(u.get("smsBalance", "0")),
        postal_city=u.get("postalCity", ""),
    )

# "1709231400000" -> 2024-03-01 00:00 IST. In UTC it would print as 2024-02-29.

Node.js: an idempotent credit transfer

The trap: addcredit returns no id and no balance, so a blind retry can credit a child twice.

import crypto from "node:crypto";

const BASE = "https://unify.smsgateway.center/SMSApi/reseller/";

async function post(action, apiKey, fields) {
  const body = new URLSearchParams(fields);            // never hand-build the body
  const res = await fetch(BASE + action, {
    method: "POST",
    headers: { apikey: apiKey, "Content-Type": "application/x-www-form-urlencoded" },
    body,
    signal: AbortSignal.timeout(15000),
  });
  return (await res.json()).response ?? {};
}

function istDate(d = new Date()) {
  return new Date(d.getTime() + 330 * 60000).toISOString().slice(0, 10);
}

async function findTransfer(parent, apiKey, child, ref) {
  const r = await post("readcredithistory", apiKey, {
    userid: parent, userloginname: child,
    fromdate: istDate(new Date(Date.now() - 86400000)), todate: istDate(), output: "json",
  });
  if (r.status !== "success") return null;
  return (r.historyList ?? []).map(x => x.history ?? {})
    .find(h => String(h.comments ?? "").startsWith(ref)) ?? null;
}

export async function transferCredits(db, parent, apiKey, child, credits, note) {
  const ref = "ct-" + crypto.randomBytes(4).toString("hex");
  await db.insertTransfer({ ref, child, credits, state: "PENDING" });   // commit first

  let state = "UNKNOWN";
  try {
    const r = await post("addcredit", apiKey, {
      userid: parent, userloginname: child, product: "SMS",
      transactiontype: "purchase", type: "purchase",      // both spellings, same value
      credits: String(credits), comment: `${ref} ${note}`, output: "json",
    });
    state = r.status === "success" ? "SENT" : "ERROR";
  } catch { /* timeout or network: state stays UNKNOWN */ }

  const row = await findTransfer(parent, apiKey, child, ref);
  if (row) {
    await db.updateTransfer(ref, { state: "CONFIRMED", historyId: String(row.uuId), balance: row.balance });
  } else {
    await db.updateTransfer(ref, { state: state === "ERROR" ? "FAILED" : "UNKNOWN" });
    // UNKNOWN is re-read later by a recovery job. It is never resent from here.
  }
  return ref;
}

Java: a resolver that fails closed

The trap: falling back to an account default sends Brand B’s message under Brand A’s sender, entity or bot.

public record SendContext(String brandKey, String channel, String loginName,
                          String secretRef, String senderKey, String dltEntityId,
                          String templateRef) {}

public final class BrandResolver {
    private final BrandRegistry registry;

    public BrandResolver(BrandRegistry registry) { this.registry = registry; }

    public SendContext resolve(String brandKey, String channel, String templateKey) {
        BrandChannel bc = registry.channel(brandKey, channel)
            .filter(BrandChannel::enabled)
            .orElseThrow(() -> new MisconfiguredBrand(brandKey + " has no enabled " + channel));

        BrandTemplate t = registry.template(brandKey, channel, templateKey)
            .orElseThrow(() -> new MisconfiguredBrand(brandKey + " has no template " + templateKey));

        if (t.senderKey() != null && !t.senderKey().equals(bc.senderKey())) {
            throw new MisconfiguredBrand("template " + templateKey
                + " is registered for " + t.senderKey() + ", brand sends as " + bc.senderKey());
        }
        if ("sms".equals(channel) && (bc.dltEntityId() == null || bc.dltEntityId().isBlank())) {
            throw new MisconfiguredBrand(brandKey + " has no DLT entity id");
        }

        MessagingAccount acct = registry.account(bc.accountId());
        if (!"active".equalsIgnoreCase(acct.status())) {
            throw new MisconfiguredBrand("account " + acct.loginName() + " is " + acct.status());
        }
        return new SendContext(brandKey, channel, acct.loginName(), acct.secretRef(),
                               bc.senderKey(), bc.dltEntityId(), t.platformRef());
    }
}

No method on BrandResolver returns a default. Every miss is an exception, and every exception is a configuration alert, not a retry.

PHP: a delivery report receiver that knows its account before it parses

The trap: one webhook per account means a shared URL cannot tell accounts apart, and a body-first parser guesses.

<?php
// Route: POST or GET /sms-dlr/{pathKey}/
function handleDlr(PDO $db, string $pathKey): void
{
    $stmt = $db->prepare('SELECT account_id FROM messaging_account WHERE webhook_path_key = ?');
    $stmt->execute([$pathKey]);
    $accountId = $stmt->fetchColumn();
    if ($accountId === false) {
        http_response_code(404);            // unknown token: reveal nothing
        return;
    }

    $raw = file_get_contents('php://input');
    if ($raw === '' || $raw === false) {
        $raw = http_build_query($_GET);     // accept either delivery style
    }

    $ins = $db->prepare(
        'INSERT INTO dlr_inbox (account_id, received_at, raw_body) VALUES (?, NOW(), ?)'
    );
    $ins->execute([$accountId, $raw]);      // persist raw, parse later

    http_response_code(200);
    echo 'OK';
}
// A worker extracts the transaction id as a STRING, joins to outbound_message
// for brand_id, and reconciles with SMSApi/reports/status method=getDlr.

Migration Checklist

Moving from “one account, one brand” to a multi-brand layout:

  • List every brand, its legal entity, budget owner and channels.
  • Mark every brand that needs a hard budget wall, its own price, its own Telegram bot or its own webhook. Those brands get child accounts.
  • Create messaging_account, brand, brand_channel and brand_template, with the Telegram partial unique index.
  • Add brand_id to the outbound message table and backfill existing rows.
  • Prefix template identifiers and WhatsApp template names with the brand key.
  • Route every send through the resolver; delete code that reads account-level defaults.
  • Pass dltEntityId and dltTemplateId on every SMS send.
  • For each child: createuser with a form encoder, readuser, verify every field, store userId as text.
  • Convert expDate, regDate and history timestamp in IST everywhere they are displayed or compared.
  • Build the credit_transfer table and the three-step transfer; never retry a transfer without reading history first.
  • Verify the adjustment spelling with a one-credit test before the first production adjustment.
  • Snapshot every child’s smsBalance daily from the first day.
  • Register a distinct webhook path token per account.
  • Map RCS bot names and bot ids, and normalised WhatsApp numbers, to brands; keep inactive bots in the registry.
  • Store each Telegram botToken in the secret store keyed by account.
  • Use generateuserpassword for human children; keep newPassword out of all logs.
  • Add a nightly drift check on the reseller endpoints, as described in the contract testing harness.

Unspecified Behaviour and How to Code Around It

The behaviours below are not pinned down by anything you can read before integrating. Each item gives the choice that stays correct whichever way the platform behaves, and none of them require waiting for an answer.

One. Treat a team sub-user’s sender IDs, templates, WhatsApp numbers and wallet as shared with the main account. Sub-users get the main account’s products and payment type. Whether their sender and template lists are separate is not something to build on. Design as if everything is shared: if they turn out to be partitioned, nothing breaks; if you assume partitioning and they are shared, one brand’s staff can use another brand’s sender.

Two. Confirm every credit transfer by reference, never by message. Whether addcredit can succeed after the client times out is unknown. The reference-plus-read-back flow is correct in both cases: a landed transfer is found and confirmed, a lost one is resent once with the same reference.

Three. Send both transactiontype and type, and both mobileno and mobileNo, with identical values. A server reads at most one key of each pair. Duplicated keys with the same value are correct under either spelling.

Four. Pin the adjustment spelling from observation. Move one credit with adjustments, read the history row’s method, and store whichever value the platform recorded. Until that check passes, route adjustments through a human.

Five. Send city, region and country, then verify with readuser. The meaning of region differs between the create table and its sample. The read-back fields postalCity, postalRegion and postalCountry tell you which input landed where, so one provisioning run settles it for good.

Six. Treat expirydate as the first instant of that day in IST. The sample expiry is exactly midnight IST. If a contract says “valid through” a date, send the following day. Being a day generous is recoverable; cutting off a client a day early is not.

Seven. Verify the parent’s balance moves on every batch of transfers. “Credit transferred” implies the parent is debited. Snapshot the parent’s smsBalance before and after; a mismatch is found on day one rather than at month end.

Eight. Assume the comment may be truncated, and put the reference first. A short reference at the start of comment survives any plausible length limit. Match with a prefix test, not equality.

Nine. Read each child by login name, and keep your own list of children. Whether readuser without a login name returns every child is unclear from its required parameters. Your messaging_account table is the list; the platform confirms each entry.

Ten. Treat any userStatus other than an observed “Active” as not sendable. Only "Active" appears in the sample. An allowlist of observed values fails closed: an unknown status pauses a brand instead of sending into a suspended account.

Eleven. Budget WhatsApp and RCS per brand in your own ledger. Credit transfers cover SMS, Voice and Miss Call. Whether a child’s WhatsApp or RCS traffic draws on a separate balance is a commercial arrangement confirmed per user. Metering in your ledger is correct under any arrangement.

Twelve. Keep a second Telegram brand out of any account that already has one. One bot per account is the rule. The unique index guarantees that a misconfiguration fails at write time instead of silently re-pointing an existing brand’s bot.


FAQs

Can one SMSGatewayCenter account send for several brands?
Yes. Sender IDs, DLT templates, WhatsApp numbers and RCS brands and bots can all coexist in one account and are selected per request. The limits are the single SMS wallet, the single SMS delivery webhook and the single Telegram bot per account.

What is the difference between a sub-user and a reseller child account?
A sub-user is a separate login under a direct customer account, with the same products, payment type and pricing as the main account. A reseller child account is a separate account created with SMSApi/reseller/createuser, with its own balance, rate plan, login and channel registrations.

Can a sub-user read the rate plan?
No. SMSApi/rateplan/read returns code 486, “Rate plans for sub-users are undefined. Please review the rates in the main account.” Sub-users inherit pricing from the main account.

Can a sub-user create more sub-users?
No. Sub-users cannot create further sub-users, and the sub-user feature is available on direct customer accounts, not reseller accounts.

Does createuser return the new user’s id?
No. The success response contains only status, msg and code. Call readuser with the same userloginname and store the quoted userId.

Why does readuser show an expiry one day before the date I set?
You are probably formatting it in UTC. expDate is epoch milliseconds for midnight IST on the chosen date; in UTC that is 18:30 on the previous day. Convert in Asia/Kolkata.

How do I avoid crediting a child account twice?
Write an intent row with a unique reference, put the reference at the start of comment, and confirm the transfer by finding that reference in readcredithistory before any retry.

Can I transfer WhatsApp or RCS credits to a child account?
The product values for addcredit and removecredit are SMS, Voice and Miss Call. Meter WhatsApp and RCS per brand in your own ledger and confirm a child’s RCS rate plan when RCS is enabled for that user.

Should reseller calls use GET or POST?
POST. All eight endpoints accept it, and credit movements should never travel in a URL.

Why does postalCity contain a number on one endpoint and a name on another?
SMSApi/account/readprofile returns identifiers such as "2707" in postalCity, while SMSApi/reseller/readuser returns names such as "Mumbai". Map the two payloads with separate types.

Can two brands share one Telegram bot?
They can share a bot only by sharing an identity, which is rarely what a brand wants. Each account has one bot, so two Telegram identities need two accounts.

How do I route SMS delivery reports when several brands share an account?
Register a URL path token per account, persist the raw body, and resolve the brand by joining the transaction id to your outbound message row, which carries brand_id.

Should I give each client its own child account or let them connect their own?
If you hold and resell credits, child accounts. If each client should own its DLT registrations, balance and relationship with the platform, have them connect their own account through OAuth.

Can a reseller add sender IDs for a child account?
Yes, in the white-label panel under “User Sender Names”. Through the API, call the SMSApi/senderid/ endpoints with the child’s own credentials.


Lets Start Multi-Brand Messaging

Running several brands, clients or business units through one messaging stack? Start with a free sandbox account on the SMSGatewayCenter demo page to test sender IDs, templates and delivery reports, or look at the bulk SMS reseller programme if you need child accounts with their own balances. For a layout review with our team, get in touch.


Latest Posts


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!