{"id":3055,"date":"2026-09-30T16:25:24","date_gmt":"2026-09-30T10:55:24","guid":{"rendered":"https:\/\/www.smsgatewaycenter.com\/blog\/?p=3055"},"modified":"2026-09-30T16:26:11","modified_gmt":"2026-09-30T10:56:11","slug":"multi-brand-messaging-architecture-accounts-sub-users","status":"publish","type":"post","link":"https:\/\/www.smsgatewaycenter.com\/blog\/multi-brand-messaging-architecture-accounts-sub-users\/","title":{"rendered":"Multi-Brand Messaging Architecture: Accounts, Sub-Users and Reseller Child Accounts for SMS, WhatsApp, RCS and Telegram"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/multi-brand-messaging-architecture-featured.webp\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"584\" src=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/multi-brand-messaging-architecture-featured-1024x584.webp\" alt=\"Three tinted geometric platforms connected to one shared base, representing several brands sharing one messaging platform\" class=\"wp-image-3056\" srcset=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/multi-brand-messaging-architecture-featured-1024x584.webp 1024w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/multi-brand-messaging-architecture-featured-300x171.webp 300w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/multi-brand-messaging-architecture-featured-768x438.webp 768w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/multi-brand-messaging-architecture-featured.webp 1200w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><figcaption class=\"wp-element-caption\">Several brands, one platform: the account boundary decides what each brand shares and what it owns.<\/figcaption><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Table of Contents<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li><a href=\"#the-short-answer\">The Short Answer<\/a><\/li>\n\n\n\n<li><a href=\"#tldr\">TL;DR<\/a><\/li>\n\n\n\n<li><a href=\"#tenancy-models\">Four Tenancy Models, Not One<\/a><\/li>\n\n\n\n<li><a href=\"#brand-owns\">What a Brand Owns on Each Channel<\/a><\/li>\n\n\n\n<li><a href=\"#account-boundary\">Six Things the Account Boundary Decides<\/a><\/li>\n\n\n\n<li><a href=\"#decision-matrix\">Decision Matrix: Where Each Brand Should Live<\/a><\/li>\n\n\n\n<li><a href=\"#brand-registry\">The Brand Registry Schema<\/a><\/li>\n\n\n\n<li><a href=\"#credential-resolver\">Resolving Credentials Per Brand at Send Time<\/a><\/li>\n\n\n\n<li><a href=\"#reseller-api\">The Reseller User API at a Glance<\/a><\/li>\n\n\n\n<li><a href=\"#create-read-back\">Creating a Child Account and Reading It Back<\/a><\/li>\n\n\n\n<li><a href=\"#user-profile-row\">The User Profile Row, Field by Field<\/a><\/li>\n\n\n\n<li><a href=\"#expiry-epoch\">Epoch Milliseconds and the Midnight IST Expiry<\/a><\/li>\n\n\n\n<li><a href=\"#credit-transfers\">Moving Credits Without Double Spending<\/a><\/li>\n\n\n\n<li><a href=\"#credit-history\">Reconciling Transfers with the User Credit History<\/a><\/li>\n\n\n\n<li><a href=\"#sender-templates\">Sender IDs, DLT Entities and Templates Per Brand<\/a><\/li>\n\n\n\n<li><a href=\"#rich-channels\">WhatsApp Numbers, RCS Brands and Telegram Bots<\/a><\/li>\n\n\n\n<li><a href=\"#webhook-routing\">One Webhook Per Account: Routing Delivery Reports<\/a><\/li>\n\n\n\n<li><a href=\"#credential-rotation\">Passwords and Credential Rotation for Child Accounts<\/a><\/li>\n\n\n\n<li><a href=\"#code-samples\">Four Code Samples, Four Traps<\/a><\/li>\n\n\n\n<li><a href=\"#migration-checklist\">Migration Checklist<\/a><\/li>\n\n\n\n<li><a href=\"#unspecified-behaviour\">Unspecified Behaviour and How to Code Around It<\/a><\/li>\n\n\n\n<li><a href=\"#faqs\">FAQs<\/a><\/li>\n<\/ol>\n\n\n\n<h2 id=\"the-short-answer\" class=\"wp-block-heading\">The Short Answer<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">When one system sends messages for several brands, the first decision is not the schema or the queue. It is where the <strong>account boundary<\/strong> 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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That gives four workable tenancy models:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>One account, many brands.<\/strong> Cheapest to run. Brands are rows in your own registry, selected by sender ID, WhatsApp number or RCS bot on every call.<\/li>\n\n\n\n<li><strong>Team sub-users under one direct account.<\/strong> Separate logins for people, not separate tenants. Sub-users carry the main account&#8217;s products and payment type, cannot create further sub-users, and inherit pricing from the main account.<\/li>\n\n\n\n<li><strong>Reseller child accounts.<\/strong> Real, separate accounts created and funded through eight <code>SMSApi\/reseller\/<\/code> endpoints. Each child has its own balance, its own RCS brand and bots, and its own login.<\/li>\n\n\n\n<li><strong>Customer-owned accounts connected through OAuth.<\/strong> For SaaS products where each customer should own the sending relationship.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<h2 id=\"tldr\" class=\"wp-block-heading\">TL;DR<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Three things are per account:<\/strong> 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.<\/li>\n\n\n\n<li><strong>Several things are per request:<\/strong> sender ID, <code>dltEntityId<\/code> and <code>dltTemplateId<\/code> on SMS, <code>wabaNumber<\/code> on WhatsApp reports, RCS bot selection through templates. One account can carry many brands on these axes.<\/li>\n\n\n\n<li><strong>Team sub-users are logins, not tenants.<\/strong> They inherit products, payment type and pricing; <code>SMSApi\/rateplan\/read<\/code> answers a sub-user with error <code>486<\/code>, &#8220;Rate plans for sub-users are undefined.&#8221;<\/li>\n\n\n\n<li><strong>Reseller child accounts are tenants.<\/strong> Eight endpoints under <code>SMSApi\/reseller\/<\/code>: <code>readuser<\/code>, <code>createuser<\/code>, <code>updateuser<\/code>, <code>generateuserpassword<\/code>, <code>resetuserpassword<\/code>, <code>readcredithistory<\/code>, <code>addcredit<\/code>, <code>removecredit<\/code>. Every response carries <code>\"api\": \"user\"<\/code>, not <code>\"reseller\"<\/code>.<\/li>\n\n\n\n<li><strong><code>createuser<\/code> returns no identifier.<\/strong> Read the new account back with <code>readuser<\/code> by <code>userloginname<\/code> and store the quoted <code>userId<\/code> it returns.<\/li>\n\n\n\n<li><strong>Profile timestamps are epoch milliseconds in quoted strings.<\/strong> The sample <code>expDate<\/code> of <code>\"1709231400000\"<\/code> is exactly midnight IST on 1 March 2024; formatted in UTC it shows 29 February.<\/li>\n\n\n\n<li><strong>Credit transfers are not idempotent and return no balance or transaction id.<\/strong> Put a unique reference in <code>comment<\/code>, and on any timeout read <code>readcredithistory<\/code> for the child before resending.<\/li>\n\n\n\n<li><strong>Credit products are <code>SMS | Voice | Miss Call<\/code>.<\/strong> WhatsApp and RCS spend per brand has to be tracked in your own ledger.<\/li>\n\n\n\n<li><strong>Parameter spellings differ between tables and samples<\/strong> (<code>mobileno<\/code> and <code>mobileNo<\/code>, <code>transactiontype<\/code> and <code>type<\/code>, <code>adjustments<\/code> and <code>adjustment<\/code>). Send both key spellings with the same value and verify by read-back.<\/li>\n\n\n\n<li><strong>Always use a form encoder.<\/strong> Hand-built bodies are how <code>&amp;region<\/code> turns into an HTML entity and how <code>&amp;=product=SMS<\/code> sends a parameter with no name.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"tenancy-models\" class=\"wp-block-heading\">Four Tenancy Models, Not One<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">&#8220;Multi-brand&#8221; 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 &#8220;several brands&#8221;, but they need different boundaries. The platform gives you four places to draw that boundary.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/diagram-multi-brand-tenancy-models.svg\"><img decoding=\"async\" src=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/diagram-multi-brand-tenancy-models.svg\" alt=\"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\" class=\"wp-image-3057\"\/><\/a><figcaption class=\"wp-element-caption\">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.<\/figcaption><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">Model 1: one account, many brands<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Every brand&#8217;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 <code>senderid<\/code>, <code>dltEntityId<\/code>, <code>dltTemplateId<\/code>, <code>wabaNumber<\/code> or RCS template code. There is one set of credentials, one wallet and one SMS webhook.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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&#8217;s campaign must not be able to drain another brand&#8217;s balance, or two brands each need their own Telegram bot.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Model 2: team sub-users under a direct account<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Sub-users are separate logins under a main direct-customer account, created in the panel under Sub-Users &gt; Create. The platform generates the password and emails it to the sub-user. According to <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/kb\/how-do-i-create-sub-users-for-my-team\/\">the sub-users knowledge base entry<\/a>, a sub-user gets &#8220;the same products, payment type and login-verification (OTP) methods as your main account&#8221; and has &#8220;no ability to create further sub-users&#8221;. 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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The rate plan endpoint confirms the pricing side: sub-users are <code>userType = 4<\/code>, &#8220;cannot access rate plans&#8221;, and &#8220;inherit pricing from the main account&#8221;. A call to <code>SMSApi\/rateplan\/read<\/code> from a sub-user returns code <code>486<\/code> with the message &#8220;Rate plans for sub-users are undefined. Please review the rates in the main account&#8221;.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Model 3: reseller child accounts<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A reseller account can create full user accounts beneath it with <code>SMSApi\/reseller\/createuser<\/code>, choosing <code>usertype<\/code> of <code>customer<\/code> or <code>reseller<\/code>. Each child is its own account: its own login, its own <code>smsBalance<\/code>, its own status and expiry date, and on RCS its own brand, bots and templates. The reseller funds children by transferring credits with <code>SMSApi\/reseller\/addcredit<\/code> and claws them back with <code>SMSApi\/reseller\/removecredit<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Model 4: customer-owned accounts through OAuth<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">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. <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/oauth-messaging-connect-customer-sms-account\/\">OAuth for messaging platforms<\/a> 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&#8217;s balance, and DLT, sender ID and template registration stay with the business that owns them.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"brand-owns\" class=\"wp-block-heading\">What a Brand Owns on Each Channel<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Before choosing a model, list what &#8220;a brand&#8221; means on each channel. On this platform the answer is different for every channel, and the names do not line up. <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/sender-identity-sms-whatsapp-rcs-telegram\/\">Sender identity across SMS, WhatsApp, RCS and Telegram<\/a> covers the field names in depth; the summary that matters for tenancy is this:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Channel<\/th><th>Brand identity unit<\/th><th>Cardinality per account<\/th><th>Selected per request by<\/th><th>Registered where<\/th><\/tr><\/thead><tbody><tr><td>SMS (India)<\/td><td>Sender ID (header) tied to a DLT principal entity and templates<\/td><td>Many sender IDs; <code>SMSApi\/senderid\/read<\/code> lists them<\/td><td><code>senderid<\/code>, plus optional <code>dltEntityId<\/code> and <code>dltTemplateId<\/code> on <code>SMSApi\/send<\/code><\/td><td>DLT portal, then the account<\/td><\/tr><tr><td>WhatsApp<\/td><td>WABA phone number<\/td><td>Several numbers addressable; <code>WAApi\/report<\/code> requires <code>wabaNumber<\/code> and analytics can group by <code>waNumber<\/code><\/td><td>The number used on the send and the <code>wabaNumber<\/code> on reads<\/td><td>Meta onboarding through the account<\/td><\/tr><tr><td>RCS<\/td><td>RCS brand, with one or more bots linked to it<\/td><td>Several brands and bots; every bot must be linked to a brand<\/td><td>The template, whose code belongs to a bot<\/td><td>Panel: RCS &gt; My RCS Brands, then bots<\/td><\/tr><tr><td>Telegram<\/td><td>One bot<\/td><td>Exactly one per account<\/td><td>Implicit: the account&#8217;s bot<\/td><td><code>rest\/tg\/v1\/setup<\/code> with <code>botName<\/code>, <code>botHandle<\/code>, <code>botToken<\/code><\/td><\/tr><tr><td>Inbound SMS<\/td><td>Keyword on a long code or short code<\/td><td>Many keywords<\/td><td>Keyword in the inbound push<\/td><td>Panel keyword setup<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Three consequences fall out of that table.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>SMS brands are cheap to co-host.<\/strong> Several sender IDs, each mapped to its own templates, is the normal state of an Indian SMS account. The <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/kb\/what-does-senderid_mismatch-senderid-mismatch-with-template-mean\/\">SENDERID_MISMATCH<\/a> outcome is what you get when a brand&#8217;s sender ID is paired with another brand&#8217;s template, so the pairing must come from one registry row, never from two independent lookups.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>RCS has a real brand object.<\/strong> The <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/kb\/what-is-an-rcs-brand-and-how-do-i-create-one\/\">RCS brand<\/a> 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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Telegram forces the split.<\/strong> 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.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"account-boundary\" class=\"wp-block-heading\">Six Things the Account Boundary Decides<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">1. The wallet<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">SMS billing fires at submission: the pricing terms state that &#8220;the applicable per-SMS rate will be deducted from your wallet while sending SMS&#8221; and that &#8220;credits are non-refundable once SMS is successfully submitted to the operator.&#8221; There is one wallet per account. If Brand A and Brand B share an account, a runaway campaign on Brand A spends Brand B&#8217;s money, and nothing on the platform stops it. <code>SMSApi\/account\/readstatus<\/code> returns one <code>smsBalance<\/code>, not one per sender ID.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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&#8217;s script and a scheduled campaign someone set up by hand. A child account is a hard wall.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">2. The SMS delivery report webhook<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">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 <a href=\"#webhook-routing\">One Webhook Per Account<\/a>), but if a brand&#8217;s delivery events must go to a different system owned by a different team, the clean answer is a separate account.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">3. The Telegram bot<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">One bot per account, configured through <code>rest\/tg\/v1\/setup<\/code>. <code>rest\/tg\/v1\/dlr<\/code> rows carry no bot field, because there is only ever one bot to report on. See <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/telegram-messaging-api-chat-id-model\/\">the Telegram messaging API<\/a> for the full contract.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">4. Pricing and the rate plan<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">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&#8217;s pricing, and the rate plan endpoint answers them with <code>486<\/code>. If two brands must be billed at different per-message rates, they cannot both be sub-users of one account.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">5. Credentials and blast radius<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">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&#8217;s child sends only as Brand A and spends only Brand A&#8217;s balance. For a platform sending on behalf of clients, that difference is often the whole argument.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">6. Feature enablement<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Some channels are switched on per account. On RCS, the reseller knowledge base states that &#8220;RCS must be enabled on each user account before the RCS menu appears&#8221; for that user. Sub-users, by contrast, get &#8220;the same products&#8221; as the main account. If one brand should have RCS and another must not, only separate accounts express that.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"decision-matrix\" class=\"wp-block-heading\">Decision Matrix: Where Each Brand Should Live<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Requirement for the brand<\/th><th>One account, many brands<\/th><th>Team sub-user<\/th><th>Reseller child account<\/th><th>Customer-owned (OAuth)<\/th><\/tr><\/thead><tbody><tr><td>Separate SMS budget that cannot be overspent<\/td><td>Soft wall only (your ledger)<\/td><td>No, shared payment type<\/td><td>Yes, own <code>smsBalance<\/code><\/td><td>Yes, customer&#8217;s wallet<\/td><\/tr><tr><td>Different per-message SMS price<\/td><td>No<\/td><td>No, inherits main pricing<\/td><td>Yes, own rate plan<\/td><td>Yes, customer&#8217;s plan<\/td><\/tr><tr><td>Own Telegram bot<\/td><td>Only one brand per account<\/td><td>No<\/td><td>Yes<\/td><td>Yes<\/td><\/tr><tr><td>Own SMS delivery webhook URL<\/td><td>No, one per account<\/td><td>No<\/td><td>Yes<\/td><td>Yes<\/td><\/tr><tr><td>Own login for staff<\/td><td>Shared login<\/td><td>Yes<\/td><td>Yes<\/td><td>Customer&#8217;s own<\/td><\/tr><tr><td>Several sender IDs and templates<\/td><td>Yes<\/td><td>Treat as shared with the main account<\/td><td>Yes<\/td><td>Yes<\/td><\/tr><tr><td>Several WhatsApp numbers<\/td><td>Yes<\/td><td>Treat as shared with the main account<\/td><td>Yes<\/td><td>Yes<\/td><\/tr><tr><td>Own RCS brand and bots<\/td><td>Yes, several brands per account<\/td><td>Treat as shared with the main account<\/td><td>Yes<\/td><td>Yes<\/td><\/tr><tr><td>API key compromise limited to one brand<\/td><td>No<\/td><td>No<\/td><td>Yes<\/td><td>Yes<\/td><\/tr><tr><td>You hold and resell credits<\/td><td>Not applicable<\/td><td>Not applicable<\/td><td>Yes<\/td><td>No<\/td><\/tr><tr><td>Lowest operational overhead<\/td><td>Best<\/td><td>Good<\/td><td>Worst<\/td><td>Depends on your product<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Three common shapes come out of the matrix:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Group brands under one legal entity<\/strong> (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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Agency or platform sending for clients who each have their own DLT registration and budget<\/strong>: a reseller account with one child account per client. The child holds the client&#8217;s sender IDs, templates and balance; your platform holds the parent credentials and the child credentials.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>SaaS product where each customer is the sender of record<\/strong>: customer-owned accounts through OAuth. You never touch their balance, and your registry stores tokens instead of passwords.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"brand-registry\" class=\"wp-block-heading\">The Brand Registry Schema<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Whatever layout you choose, the application needs one place that answers &#8220;for brand X on channel Y, which account, which credentials and which identity do I use?&#8221; That is the brand registry. It is the multi-brand counterpart of <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/outbound-message-table-schema-design\/\">the outbound message table<\/a>: every outbound row should carry a <code>brand_id<\/code>, and every <code>brand_id<\/code> resolves through the registry.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>-- One row per account you can send through, whatever tenancy model it uses.\nCREATE TABLE messaging_account (\n  account_id        BIGSERIAL PRIMARY KEY,\n  tenancy_model     TEXT NOT NULL CHECK (tenancy_model IN\n                      ('shared','subuser','child','oauth')),\n  parent_account_id BIGINT REFERENCES messaging_account(account_id),\n  login_name        TEXT NOT NULL,          -- userid \/ userloginname\n  platform_user_id  TEXT,                   -- quoted userId from readuser, kept as text\n  secret_ref        TEXT NOT NULL,          -- pointer into your secret store, never the key\n  webhook_path_key  TEXT UNIQUE,            -- random token in this account's DLR URL\n  status            TEXT NOT NULL,          -- last observed userStatus\n  expires_at        TIMESTAMPTZ,            -- from expDate, see the epoch section\n  last_synced_at    TIMESTAMPTZ\n);\n\n-- One row per brand. A brand lives in exactly one account per channel.\nCREATE TABLE brand (\n  brand_id     BIGSERIAL PRIMARY KEY,\n  brand_key    TEXT UNIQUE NOT NULL,        -- 'acme-retail', stable, human readable\n  legal_entity TEXT NOT NULL,\n  budget_owner TEXT NOT NULL\n);\n\n-- Channel identity per brand. The pairing rules live here, nowhere else.\nCREATE TABLE brand_channel (\n  brand_id        BIGINT NOT NULL REFERENCES brand(brand_id),\n  channel         TEXT NOT NULL CHECK (channel IN ('sms','whatsapp','rcs','telegram')),\n  account_id      BIGINT NOT NULL REFERENCES messaging_account(account_id),\n  sender_key      TEXT,     -- SMS senderid, WhatsApp number, RCS bot name\n  dlt_entity_id   TEXT,     -- SMS only, text: long numeric\n  enabled         BOOLEAN NOT NULL DEFAULT FALSE,\n  PRIMARY KEY (brand_id, channel)\n);\n\n-- Templates are owned by a brand and a sender, never by a channel alone.\nCREATE TABLE brand_template (\n  brand_id        BIGINT NOT NULL,\n  channel         TEXT NOT NULL,\n  template_key    TEXT NOT NULL,            -- your name for it\n  platform_ref    TEXT NOT NULL,            -- dltTemplateId, WA name+language, RCS templateCode\n  sender_key      TEXT,                     -- the sender this template is registered with\n  PRIMARY KEY (brand_id, channel, template_key),\n  FOREIGN KEY (brand_id, channel) REFERENCES brand_channel(brand_id, channel)\n);\n\n-- A second Telegram brand on the same account is a design error. Make it impossible.\nCREATE UNIQUE INDEX one_telegram_brand_per_account\n  ON brand_channel(account_id) WHERE channel = 'telegram';<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Five design choices in that schema matter more than the column names.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The primary key on <code>brand_channel<\/code> is <code>(brand_id, channel)<\/code>.<\/strong> A brand lives in exactly one account per channel. If you allow a brand&#8217;s SMS to go out of two accounts, delivery reports, balances and DLT pairings split across two places and reconciliation never closes.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Templates carry a <code>sender_key<\/code>.<\/strong> 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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Secrets are references.<\/strong> 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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Platform identifiers are text.<\/strong> <code>userId<\/code> arrives quoted (<code>\"6\"<\/code>), credit history <code>uuId<\/code> 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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The partial unique index encodes the Telegram rule.<\/strong> The database, not a code review, stops a second brand from claiming an account&#8217;s only bot.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"credential-resolver\" class=\"wp-block-heading\">Resolving Credentials Per Brand at Send Time<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The resolver turns <code>(brand_key, channel, template_key)<\/code> 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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Four rules keep it safe.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Resolve once, pass explicitly.<\/strong> The resolver returns a complete, immutable send context. Downstream code never looks up &#8220;the default sender ID&#8221; or &#8220;the account&#8217;s DLT entity&#8221; on its own. Account-level defaults are exactly the thing that is wrong for the second brand in a shared account.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Send <code>dltEntityId<\/code> and <code>dltTemplateId<\/code> on every SMS.<\/strong> <code>SMSApi\/send<\/code> 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&#8217;s own entity and template are used.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Fail closed when a brand has no identity on a channel.<\/strong> If Brand B has no <code>brand_channel<\/code> row for RCS, the resolver raises. It does not fall back to Brand A&#8217;s bot, and it does not fall back to the account&#8217;s first bot. A missing mapping is a configuration bug, and a message sent under the wrong brand is worse than one not sent.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Authenticate with the <code>apikey<\/code> header and still send <code>userid<\/code>.<\/strong> The <code>apikey<\/code> header is accepted on every endpoint. HTTP header names are case-insensitive, so the <code>apiKey<\/code> spelling in the reseller and <code>SMSApi\/<\/code> tables is the same header. Send the account&#8217;s <code>userid<\/code> alongside it. In a layout with child accounts, the resolver picks the child&#8217;s <code>userid<\/code> and key for sends and the parent&#8217;s for reseller calls, and those two credential sets must never be swapped.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A resolved context looks like this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"brand_key\": \"acme-retail\",\n  \"channel\": \"sms\",\n  \"account\": { \"login_name\": \"acmechild01\", \"secret_ref\": \"vault:msg\/acmechild01\" },\n  \"base_url\": \"https:\/\/unify.smsgateway.center\/\",\n  \"sender_key\": \"ACMERT\",\n  \"dlt_entity_id\": \"1201159XXXXXXXXXXXX\",\n  \"template\": { \"template_key\": \"order-shipped\", \"platform_ref\": \"1207161XXXXXXXXXXXX\" }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The two DLT values are placeholders; real ones are long numerics and are stored as text.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"reseller-api\" class=\"wp-block-heading\">The Reseller User API at a Glance<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Child accounts are created and funded through eight endpoints under <code>https:\/\/unify.smsgateway.center\/SMSApi\/reseller\/<\/code>. All of them take the parent&#8217;s credentials plus a <code>userloginname<\/code> naming the child, and all of them return the familiar <code>response<\/code> envelope with <code>\"api\": \"user\"<\/code> and a quoted <code>\"code\": \"200\"<\/code> on success.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Endpoint<\/th><th>Method on its page<\/th><th>Purpose<\/th><th>Required parameters beyond credentials<\/th><th>Success <code>msg<\/code><\/th><th>Returns an identifier?<\/th><\/tr><\/thead><tbody><tr><td><code>SMSApi\/reseller\/readuser<\/code><\/td><td>POST and GET<\/td><td>Read a child&#8217;s profile<\/td><td><code>userloginname<\/code>, <code>output<\/code><\/td><td>&#8220;success&#8221;<\/td><td>Yes, <code>userId<\/code> inside <code>userList[i].user<\/code><\/td><\/tr><tr><td><code>SMSApi\/reseller\/createuser<\/code><\/td><td>POST only<\/td><td>Create a child account<\/td><td><code>userloginname<\/code>, <code>usertype<\/code>, <code>email<\/code>, <code>mobileno<\/code>, <code>fullname<\/code>, <code>address<\/code>, <code>region<\/code>, <code>expirydate<\/code>, <code>output<\/code><\/td><td>&#8220;User created successfully.&#8221;<\/td><td>No<\/td><\/tr><tr><td><code>SMSApi\/reseller\/updateuser<\/code><\/td><td>POST only<\/td><td>Update a child&#8217;s details<\/td><td>Same set as create<\/td><td>&#8220;User update successfully.&#8221;<\/td><td>No<\/td><\/tr><tr><td><code>SMSApi\/reseller\/generateuserpassword<\/code><\/td><td>POST only<\/td><td>Email the child a reset link<\/td><td><code>userloginname<\/code>, <code>output<\/code><\/td><td>&#8220;Password changed successfully.&#8221;<\/td><td>No<\/td><\/tr><tr><td><code>SMSApi\/reseller\/resetuserpassword<\/code><\/td><td>POST only<\/td><td>Set the child&#8217;s password directly<\/td><td><code>userloginname<\/code>, <code>newPassword<\/code>, <code>output<\/code><\/td><td>&#8220;Password changed successfully.&#8221;<\/td><td>No<\/td><\/tr><tr><td><code>SMSApi\/reseller\/readcredithistory<\/code><\/td><td>POST and GET<\/td><td>Read a child&#8217;s credit movements<\/td><td><code>userloginname<\/code>, <code>fromdate<\/code>, <code>todate<\/code>, <code>output<\/code><\/td><td>&#8220;success&#8221;<\/td><td>Yes, <code>uuId<\/code> per history row<\/td><\/tr><tr><td><code>SMSApi\/reseller\/addcredit<\/code><\/td><td>POST and GET<\/td><td>Transfer credits to a child<\/td><td><code>userloginname<\/code>, <code>product<\/code>, <code>transactiontype<\/code>, <code>credits<\/code>, <code>comment<\/code>, <code>output<\/code><\/td><td>&#8220;Credit transferred successfully.&#8221;<\/td><td>No<\/td><\/tr><tr><td><code>SMSApi\/reseller\/removecredit<\/code><\/td><td>POST and GET<\/td><td>Take credits back from a child<\/td><td>Same set as add<\/td><td>&#8220;Credit removed successfully.&#8221;<\/td><td>No<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Five things in that table shape every client you write against it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Use POST for everything.<\/strong> 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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The <code>api<\/code> field says <code>user<\/code>, the path says <code>reseller<\/code>.<\/strong> If your client asserts that <code>response.api<\/code> matches the path segment, it will reject every successful reply. Check <code>response.status<\/code> and nothing else to decide success, the same rule that applies across the rest of the API.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Writes return no identifier.<\/strong> <code>createuser<\/code> does not return the new <code>userId<\/code>, 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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Two success messages lie about what happened.<\/strong> <code>generateuserpassword<\/code> sends the child an email with a reset link, yet its success <code>msg<\/code> reads &#8220;Password changed successfully.&#8221; Treat it as &#8220;reset email dispatched&#8221; and never tell an operator that the password has changed. Never parse <code>msg<\/code> at all; branch on <code>status<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Read the child with <code>userloginname<\/code>, even for a listing.<\/strong> The <code>readuser<\/code> page describes retrieving &#8220;a list of all users&#8221;, but <code>userloginname<\/code> sits among the required parameters and the sample returns one row with <code>count<\/code> 1. Keep your own list of children in <code>messaging_account<\/code> and read each one by name.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"create-read-back\" class=\"wp-block-heading\">Creating a Child Account and Reading It Back<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step 1: create<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The create table and the sample on the same page spell some parameters differently:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Concept<\/th><th>Parameter table<\/th><th>Sample request body<\/th><\/tr><\/thead><tbody><tr><td>Mobile number<\/td><td><code>mobileno<\/code> (&#8220;Should be integer&#8221;)<\/td><td><code>mobileNo=919999xxxxxx<\/code><\/td><\/tr><tr><td>City<\/td><td><code>region<\/code>, described as &#8220;City&#8221;<\/td><td><code>city=city name<\/code><\/td><\/tr><tr><td>State<\/td><td>not listed<\/td><td><code>region=state name<\/code><\/td><\/tr><tr><td>Country<\/td><td>not listed<\/td><td><code>country=country name<\/code><\/td><\/tr><tr><td>Expiry<\/td><td><code>expirydate<\/code>, YYYY-MM-DD<\/td><td><code>expirydate=2019-10-01<\/code><\/td><\/tr><tr><td>Login name<\/td><td><code>userloginname<\/code><\/td><td><code>userloginname=...<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The table and sample agree on <code>userloginname<\/code>, <code>usertype<\/code>, <code>email<\/code>, <code>fullname<\/code>, <code>address<\/code> and <code>expirydate<\/code>. For the rest, the safe approach is to send both spellings with the same value: <code>mobileno<\/code> and <code>mobileNo<\/code> carrying the same number. A server that reads one key ignores the other, so the request is correct under either reading.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>region<\/code> 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 (<code>city<\/code>, <code>region<\/code> as the state, <code>country<\/code>) and then verify with the read-back in step 2. The profile row returns <code>postalCity<\/code>, <code>postalRegion<\/code> and <code>postalCountry<\/code> separately, so one read tells you which input landed where.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Build the body with a form encoder. The sample strings are concatenated by hand, and a hand-built string containing <code>&amp;region<\/code> is exactly where an HTML renderer or a careless copy turns <code>&amp;reg<\/code> into a registered-trademark sign. A form encoder also handles spaces in <code>fullname<\/code> and <code>address<\/code>, which the sample sends unencoded.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step 2: read back<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>POST https:\/\/unify.smsgateway.center\/SMSApi\/reseller\/readuser\napikey: &lt;parent key&gt;\nContent-Type: application\/x-www-form-urlencoded\n\nuserid=&lt;parent login&gt;&amp;userloginname=acmechild01&amp;output=json<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Take <code>response.userList[0].user.userId<\/code>, keep it as text, and write it into <code>messaging_account.platform_user_id<\/code>. From here on your registry knows the child by both its login name and its platform id.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step 3: verify<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Compare the read-back row against the create request field by field: <code>userType<\/code> against <code>usertype<\/code> (case-insensitively, since the sample returns <code>\"Reseller\"<\/code> for an input of <code>reseller<\/code>), <code>emailId<\/code> against <code>email<\/code>, <code>mobileNo<\/code> against the mobile number, <code>postalCity<\/code>, <code>postalRegion<\/code> and <code>postalCountry<\/code> against what you sent, and <code>expDate<\/code> against <code>expirydate<\/code> (after the conversion in <a href=\"#expiry-epoch\">the epoch section<\/a>). A mismatch is a provisioning failure: alert, do not start sending.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If step 1 times out, do not resend it. Go straight to step 2. If <code>readuser<\/code> finds the login name, the create succeeded and you continue with step 3. If it does not, resend the create with the same <code>userloginname<\/code>. Every reseller call addresses the child by <code>userloginname<\/code>, 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.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"user-profile-row\" class=\"wp-block-heading\">The User Profile Row, Field by Field<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>readuser<\/code> sample response is the most information-dense payload in the reseller family:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"response\": {\n    \"api\": \"user\",\n    \"action\": \"readuser\",\n    \"status\": \"success\",\n    \"msg\": \"success\",\n    \"code\": \"200\",\n    \"count\": 1,\n    \"userList\": &#91;\n      {\n        \"user\": {\n          \"userId\": \"6\",\n          \"userName\": \"myuser\",\n          \"userType\": \"Reseller\",\n          \"emailId\": \"user@example.com\",\n          \"mobileNo\": \"919999999999\",\n          \"domainName\": \"smssssssssssss.com\",\n          \"expDate\": \"1709231400000\",\n          \"regDate\": \"1546740446000\",\n          \"userStatus\": \"Active\",\n          \"enableCMS\": \"Inactive\",\n          \"fullName\": \"\",\n          \"postalAddress\": \"\",\n          \"postalCity\": \"Mumbai\",\n          \"postalCountry\": \"India\",\n          \"postalRegion\": \"Maharashtra\",\n          \"smppEnabled\": \"1\",\n          \"accountType\": \"TRANSACTIONAL\",\n          \"smsBalance\": \"3900\"\n        }\n      }\n    ]\n  }\n}<\/code><\/pre>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Field<\/th><th>Wire type<\/th><th>Meaning and handling<\/th><\/tr><\/thead><tbody><tr><td><code>userId<\/code><\/td><td>Quoted digits<\/td><td>Platform id of the child. Store as text.<\/td><\/tr><tr><td><code>userName<\/code><\/td><td>String<\/td><td>Login name; equals <code>userloginname<\/code>.<\/td><\/tr><tr><td><code>userType<\/code><\/td><td>String, capitalised<\/td><td><code>\"Reseller\"<\/code> in the sample; input values are lower case. Compare case-insensitively.<\/td><\/tr><tr><td><code>emailId<\/code><\/td><td>String<\/td><td>Contact email.<\/td><\/tr><tr><td><code>mobileNo<\/code><\/td><td>Quoted digits with country code<\/td><td>Store as text. Never parse as a number.<\/td><\/tr><tr><td><code>domainName<\/code><\/td><td>String<\/td><td>The white-label domain associated with the user.<\/td><\/tr><tr><td><code>expDate<\/code><\/td><td>Quoted epoch milliseconds<\/td><td>Account expiry. See the next section.<\/td><\/tr><tr><td><code>regDate<\/code><\/td><td>Quoted epoch milliseconds<\/td><td>Registration time. <code>\"1546740446000\"<\/code> is 2019-01-06 07:37:26 IST.<\/td><\/tr><tr><td><code>userStatus<\/code><\/td><td>String<\/td><td><code>\"Active\"<\/code> in the sample. Treat any other value as not sendable.<\/td><\/tr><tr><td><code>enableCMS<\/code><\/td><td>String<\/td><td><code>\"Inactive\"<\/code> here.<\/td><\/tr><tr><td><code>postalCity<\/code>, <code>postalRegion<\/code>, <code>postalCountry<\/code><\/td><td>String names<\/td><td><code>\"Mumbai\"<\/code>, <code>\"Maharashtra\"<\/code>, <code>\"India\"<\/code>.<\/td><\/tr><tr><td><code>smppEnabled<\/code><\/td><td>Quoted <code>\"1\"<\/code><\/td><td>Flag as a string.<\/td><\/tr><tr><td><code>accountType<\/code><\/td><td>String<\/td><td><code>\"TRANSACTIONAL\"<\/code> in the sample.<\/td><\/tr><tr><td><code>smsBalance<\/code><\/td><td>Quoted integer<\/td><td>The child&#8217;s SMS credits. Parse with an integer parser that rejects decimals.<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Two fields in that row share a name with fields on the account&#8217;s own profile endpoint but not their encoding. On <code>SMSApi\/account\/readprofile<\/code>, <code>postalCity<\/code> comes back as <code>\"2707\"<\/code>, <code>postalCountry<\/code> as <code>\"101\"<\/code> and <code>postalRegion<\/code> as <code>\"22\"<\/code>: numeric identifiers in a name-like field. On <code>readuser<\/code> the same three fields carry names. <code>enableCMS<\/code> is <code>\"1\"<\/code> on <code>readprofile<\/code> and <code>\"Inactive\"<\/code> on <code>readuser<\/code>. 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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For the same reason, never compare a child&#8217;s <code>postalCity<\/code> from <code>readuser<\/code> with the parent&#8217;s <code>postalCity<\/code> from <code>readprofile<\/code>. They are different kinds of value that happen to share a key.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"expiry-epoch\" class=\"wp-block-heading\">Epoch Milliseconds and the Midnight IST Expiry<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><code>createuser<\/code> and <code>updateuser<\/code> take <code>expirydate<\/code> as a calendar date, <code>YYYY-MM-DD<\/code>. <code>readuser<\/code> returns <code>expDate<\/code> as epoch milliseconds in a quoted string. The sample value is worth converting by hand:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Value<\/th><th>As UTC<\/th><th>As IST (UTC+05:30)<\/th><\/tr><\/thead><tbody><tr><td><code>expDate<\/code> <code>\"1709231400000\"<\/code><\/td><td>2024-02-29 18:30:00<\/td><td>2024-03-01 00:00:00<\/td><\/tr><tr><td><code>regDate<\/code> <code>\"1546740446000\"<\/code><\/td><td>2019-01-06 02:07:26<\/td><td>2019-01-06 07:37:26<\/td><\/tr><tr><td>credit history <code>timestamp<\/code> <code>\"1570868345603\"<\/code><\/td><td>2019-10-12 08:19:05.603<\/td><td>2019-10-12 13:49:05.603<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">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:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Formatting in UTC shows the wrong day.<\/strong> A dashboard running in a UTC container that prints <code>expDate<\/code> as a date shows 29 February for an account you set to expire on 1 March. An operator who &#8220;corrects&#8221; it by setting 2 March has now given the account an extra day.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Round-trip checks must convert in IST.<\/strong> In the verify step, convert <code>expDate<\/code> to a date in <code>Asia\/Kolkata<\/code> before comparing it with the <code>expirydate<\/code> you sent.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Decide what &#8220;expires on 1 March&#8221; means for your brand.<\/strong> 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 &#8220;valid through 1 March&#8221;, send 2 March.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Parse the string, then the number.<\/strong> The values are quoted. In JavaScript, <code>Number(\"1709231400000\")<\/code> is exact because thirteen-digit millisecond values are far below 2^53, but code that feeds the raw string into a date constructor gets <code>Invalid Date<\/code>. Convert explicitly.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The same convention applies to every timestamp in the reseller family, including the credit history rows in the next two sections.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"credit-transfers\" class=\"wp-block-heading\">Moving Credits Without Double Spending<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><code>addcredit<\/code> and <code>removecredit<\/code> 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.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/diagram-credit-transfer-read-back.svg\"><img decoding=\"async\" src=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/diagram-credit-transfer-read-back.svg\" alt=\"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\" class=\"wp-image-3058\"\/><\/a><figcaption class=\"wp-element-caption\">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.<\/figcaption><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">The parameters, and where the table and sample disagree<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Concept<\/th><th>Parameter table<\/th><th>Sample request body<\/th><\/tr><\/thead><tbody><tr><td>Child account<\/td><td><code>userloginname<\/code><\/td><td><code>userloginname=YourUserLoginname<\/code><\/td><\/tr><tr><td>Product<\/td><td><code>product<\/code>: <code>SMS<\/code>, <code>Voice<\/code> or <code>Miss Call<\/code><\/td><td><code>product=SMS<\/code> (on the add page, preceded by a stray <code>&amp;=<\/code>)<\/td><\/tr><tr><td>Transaction type<\/td><td><code>transactiontype<\/code>: <code>purchase<\/code> or <code>adjustments<\/code><\/td><td><code>type=purchase<\/code> on add, <code>type=adjustment<\/code> on remove<\/td><\/tr><tr><td>Amount<\/td><td><code>credits<\/code>, integer<\/td><td><code>credits=10000<\/code><\/td><\/tr><tr><td>Note<\/td><td><code>comment<\/code><\/td><td><code>comment=...<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Apply the same rule as on create: send <code>transactiontype<\/code> and <code>type<\/code> with the same value, so the request is right whichever key the server reads. The value spelling (<code>adjustments<\/code> or <code>adjustment<\/code>) cannot be hedged the same way, because you can only send one value per key. For top-ups, use <code>purchase<\/code>, where the table and both samples agree. Before your first adjustment in production, move one credit with the table spelling, read the child&#8217;s history, and check that the row&#8217;s <code>method<\/code> reflects an adjustment; pin whichever spelling produced it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The stray <code>&amp;=<\/code> 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.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The product list decides what you can allocate<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>product<\/code> accepts <code>SMS<\/code>, <code>Voice<\/code> and <code>Miss Call<\/code>. WhatsApp and RCS are not in that list, even though the page intro mentions WhatsApp. For a multi-brand platform this means:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>SMS budgets per child<\/strong> can be enforced by the platform, through transfers.<\/li>\n\n\n\n<li><strong>WhatsApp and RCS spend per brand<\/strong> has to be measured from delivery and analytics data and enforced in your own ledger. <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/reconciling-messaging-invoice-four-channels\/\">Reconciling a messaging invoice across four channels<\/a> covers where per-message cost appears on each channel.<\/li>\n\n\n\n<li><strong>Rate plans for a child&#8217;s RCS traffic<\/strong> are confirmed with the platform when RCS is enabled for that user, per the reseller RCS knowledge base.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">The three-step transfer<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Step 1. Record intent before calling.<\/strong> Write a row to your own <code>credit_transfer<\/code> table with a fresh reference such as <code>ct-7f3a9c21<\/code>, the child, product, type, amount and state <code>PENDING<\/code>. Commit before the HTTP call. If the process dies mid-call, the row survives and tells the recovery job what to look for.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Step 2. POST the transfer with the reference in <code>comment<\/code>.<\/strong> Put the reference at the start of the comment, for example <code>comment=ct-7f3a9c21 monthly top-up acme-retail<\/code>. The comment is the only free-text field that travels with the transfer, and credit history rows return a <code>comments<\/code> field.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Step 3. Confirm by reading the child&#8217;s credit history.<\/strong> Whether the POST returned success, failed or timed out, call <code>readcredithistory<\/code> for the child over a window covering the call, and look for a row whose <code>comments<\/code> contains the reference. If it is there, mark the transfer <code>CONFIRMED<\/code> and store the row&#8217;s <code>uuId<\/code> and <code>balance<\/code>. If it is absent and the POST clearly failed, mark it <code>FAILED<\/code>. If it is absent after a timeout, wait and read again before concluding anything.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Only one path leads to a resend: the reference is still absent after a second read, well after the call. Resend with the <strong>same<\/strong> reference. If both attempts eventually land, the two rows share one reference, which makes the duplicate visible and reversible with a single <code>removecredit<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">State machine<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>State<\/th><th>Entered when<\/th><th>Next action<\/th><\/tr><\/thead><tbody><tr><td><code>PENDING<\/code><\/td><td>Intent row written<\/td><td>POST the transfer<\/td><\/tr><tr><td><code>SENT<\/code><\/td><td>POST returned <code>status: success<\/code><\/td><td>Read history, match reference<\/td><\/tr><tr><td><code>UNKNOWN<\/code><\/td><td>POST timed out or the connection dropped<\/td><td>Wait, then read history; never resend from here directly<\/td><\/tr><tr><td><code>CONFIRMED<\/code><\/td><td>History row with the reference found<\/td><td>Store <code>uuId<\/code>, <code>balance<\/code>; done<\/td><\/tr><tr><td><code>FAILED<\/code><\/td><td>POST returned <code>status: error<\/code> and history shows no row<\/td><td>Alert; safe to create a new intent<\/td><\/tr><tr><td><code>DUPLICATE<\/code><\/td><td>Two history rows carry the same reference<\/td><td>Reverse one with <code>removecredit<\/code>, reference it in the comment<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">This is the same idempotency discipline as <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/message-idempotency-preventing-duplicate-sends\/\">preventing duplicate sends<\/a>, applied to money instead of messages: the client owns the key, because the server does not.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"credit-history\" class=\"wp-block-heading\">Reconciling Transfers with the User Credit History<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><code>SMSApi\/reseller\/readcredithistory<\/code> is the read side of every transfer. Unlike the account&#8217;s own <code>SMSApi\/account\/readcredithistory<\/code>, which has no date range, the reseller version requires <code>fromdate<\/code> and <code>todate<\/code> in <code>YYYY-MM-DD<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"response\": {\n    \"api\": \"user\",\n    \"action\": \"readcredithistory\",\n    \"status\": \"success\",\n    \"msg\": \"success\",\n    \"code\": \"200\",\n    \"count\": 1,\n    \"historyList\": &#91;\n      {\n        \"history\": {\n          \"uuId\": \"3555847512901782\",\n          \"type\": \"CREDIT\",\n          \"method\": \"purchase\",\n          \"balance\": \"3800\",\n          \"amount\": \"100\",\n          \"comments\": \"testing\",\n          \"timestamp\": \"1570868345603\"\n        }\n      }\n    ]\n  }\n}<\/code><\/pre>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Field<\/th><th>Wire type<\/th><th>Handling<\/th><\/tr><\/thead><tbody><tr><td><code>uuId<\/code><\/td><td>Quoted, sixteen digits in the sample<\/td><td>The history row id. Store as text; sibling ids elsewhere in the API run to nineteen digits.<\/td><\/tr><tr><td><code>type<\/code><\/td><td>String<\/td><td><code>\"CREDIT\"<\/code> in the sample. Direction of the movement.<\/td><\/tr><tr><td><code>method<\/code><\/td><td>String<\/td><td><code>\"purchase\"<\/code> here; mirrors the transaction type you sent.<\/td><\/tr><tr><td><code>balance<\/code><\/td><td>Quoted integer<\/td><td>Balance after the movement.<\/td><\/tr><tr><td><code>amount<\/code><\/td><td>Quoted integer<\/td><td>Credits moved.<\/td><\/tr><tr><td><code>comments<\/code><\/td><td>String<\/td><td>Your <code>comment<\/code>. The reference lives here.<\/td><\/tr><tr><td><code>timestamp<\/code><\/td><td>Quoted epoch milliseconds<\/td><td>Convert in IST as in the previous section.<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The reseller history&#8217;s numbers are quoted strings; the account&#8217;s own credit history returns integer credits. A shared parser for &#8220;credit history rows&#8221; across the two endpoints must accept both, or better, the two endpoints get two row types.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Daily reconciliation per child<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Once a day, for each child account:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Read <code>readcredithistory<\/code> for yesterday&#8217;s IST date, both bounds the same day.<\/li>\n\n\n\n<li>Match every row with a reference in <code>comments<\/code> against your <code>credit_transfer<\/code> 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.<\/li>\n\n\n\n<li>Check continuity: consecutive rows should chain, each <code>balance<\/code> equal to the previous row&#8217;s <code>balance<\/code> plus or minus <code>amount<\/code>. A gap means sending consumed credits between movements, which is expected; a jump in the opposite direction is not.<\/li>\n\n\n\n<li>Read <code>readuser<\/code> and store <code>smsBalance<\/code> as the day&#8217;s closing snapshot. Keep it as history; the platform gives you the current balance, not yesterday&#8217;s.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Store the snapshots from the first day. A daily row costs almost nothing and turns &#8220;what was Brand A&#8217;s balance on the 14th?&#8221; from an unanswerable question into a query.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Checking the parent side<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The success message for <code>addcredit<\/code> reads &#8220;Credit transferred successfully&#8221;, which describes a movement from the parent to the child. Read the parent&#8217;s own balance with <code>SMSApi\/account\/readstatus<\/code> before and after a batch of transfers and check that the parent&#8217;s <code>smsBalance<\/code> 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.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"sender-templates\" class=\"wp-block-heading\">Sender IDs, DLT Entities and Templates Per Brand<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">In a shared account<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Several brands&#8217; sender IDs sit side by side in <code>SMSApi\/senderid\/read<\/code>, and several brands&#8217; templates sit side by side in <code>SMSApi\/template\/read<\/code>. Nothing in either list says which brand a row belongs to. The sender list returns <code>sId<\/code>, <code>senderName<\/code>, <code>isEnabled<\/code> and <code>addTime<\/code>; the template list returns <code>mtId<\/code>, <code>identifier<\/code>, <code>template<\/code>, <code>dltTemplateId<\/code>, <code>senderIds<\/code> and <code>status<\/code>. The brand is your concept, so the mapping lives in <code>brand_channel<\/code> and <code>brand_template<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Three practices keep shared accounts clean:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Name templates with a brand prefix.<\/strong> Use <code>identifier<\/code> values such as <code>acme-retail.order-shipped<\/code>. When someone reads the template list in the panel, the brand is obvious, and your sync job can flag rows with no known prefix.<\/li>\n\n\n\n<li><strong>Validate the sender and template pair before sending.<\/strong> The resolver checks that the chosen sender appears in the template&#8217;s <code>senderIds<\/code>. That catches in your code what the platform would report as SENDERID_MISMATCH after the fact.<\/li>\n\n\n\n<li><strong>Pass <code>dltEntityId<\/code> and <code>dltTemplateId<\/code> on every send.<\/strong> 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.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/message-template-management-four-channels\/\">Message template management across four channels<\/a> covers the template lifecycle, typed DLT variable tags and read-back rules on every channel.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">In a child account<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">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&#8217;s lists can treat every row as belonging to the brand.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A reseller can add sender names on behalf of a user in the white-label panel, under &#8220;User Sender Names&#8221;, per <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/kb\/can-i-add-sender-names-for-my-users-as-a-reseller\/\">the reseller sender names knowledge base entry<\/a>. Through the API, sender IDs and templates are managed with the <code>SMSApi\/senderid\/<\/code> and <code>SMSApi\/template\/<\/code> endpoints called with the child&#8217;s own credentials. Your provisioning job therefore switches credential sets partway through: parent credentials for <code>createuser<\/code> and <code>addcredit<\/code>, child credentials for sender ID and template setup.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Moving a brand between accounts<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">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&#8217;s rows until delivery reports for traffic sent before the switch have settled, because those reports still arrive through the old account.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"rich-channels\" class=\"wp-block-heading\">WhatsApp Numbers, RCS Brands and Telegram Bots<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">WhatsApp<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The WhatsApp surface is built to address more than one number per account. <code>WAApi\/report<\/code> requires <code>wabaNumber<\/code>, and <code>rest\/wa\/v1\/analytics<\/code> accepts <code>groupBy=waNumber<\/code>. In a shared account, each brand gets its own WhatsApp number, and your registry maps brand to number.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Two traps apply specifically to multi-brand WhatsApp.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The number changes type between endpoints.<\/strong> Report rows carry <code>wabaNumber<\/code> unquoted, analytics delivery rows carry <code>waNumber<\/code> quoted, and inbox rows carry a quoted <code>wabaNumber<\/code>. A brand lookup keyed on &#8220;the number&#8221; must normalise all three to one text form, digits only with country code, before comparing.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Media writes are number-scoped; media reads are account-scoped.<\/strong> Brand A&#8217;s uploaded media is visible when you list media for the account. Tag media in your own store with the brand, and never pick &#8220;the latest uploaded image&#8221; from the account list.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Template names are lower-cased and deleted by name and language, so prefix WhatsApp template names with the brand too. In a shared account, <code>acme_order_shipped<\/code> and <code>zenith_order_shipped<\/code> cannot collide.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">RCS<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">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 <code>rest\/rcs\/v1\/dlr<\/code> carry <code>botName<\/code>, and inbox rows from <code>rest\/rcs\/v1\/inbox<\/code> carry an unquoted <code>botId<\/code>, so routing an RCS event to a brand needs both mappings in your registry: bot name to brand and bot id to brand.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>rest\/rcs\/v1\/bots<\/code> 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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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 <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/kb\/what-rcs-features-are-available-to-me-as-a-reseller\/\">the reseller RCS features entry<\/a>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Telegram<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">One bot per account, set with <code>rest\/tg\/v1\/setup<\/code>. <code>botToken<\/code> is write-only: the setup read returns <code>botName<\/code> and <code>botHandle<\/code>, never the token. For a multi-brand layout:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>One Telegram brand per account, enforced by the partial unique index in the registry.<\/li>\n\n\n\n<li>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.<\/li>\n\n\n\n<li>Store each bot&#8217;s token in your secret store keyed by account, because you cannot read it back from the platform if you lose it.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"webhook-routing\" class=\"wp-block-heading\">One Webhook Per Account: Routing Delivery Reports<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Give every account its own URL path<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Even when all accounts deliver to the same service, register each with a distinct path containing a random token:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>https:\/\/hooks.example.com\/sms-dlr\/k7Qm2xV9pLr4\/   &lt;- shared account\nhttps:\/\/hooks.example.com\/sms-dlr\/Zp8wT3nB6yHc\/   &lt;- child for acme-retail\nhttps:\/\/hooks.example.com\/sms-dlr\/Rf5jK1sD0qMu\/   &lt;- child for zenith-bank<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The token is <code>messaging_account.webhook_path_key<\/code>. 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.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Resolve the brand from your own outbound row<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Within an account, resolve the brand by joining the report to your outbound message row by transaction id. Your outbound row already carries <code>brand_id<\/code>. 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.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Persist raw, then reconcile<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The push payload&#8217;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 <code>SMSApi\/reports\/status<\/code> with <code>method=getDlr<\/code>, which returns a stable row shape including <code>uuId<\/code>, <code>mobileNo<\/code>, <code>submitTime<\/code> and <code>status<\/code>. <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/delivery-report-ingestion-system-of-record\/\">Delivery report ingestion as a system of record<\/a> walks through that pipeline.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The other channels poll<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">WhatsApp, RCS and Telegram inbound traffic is read by polling <code>rest\/wa\/v1\/inbox<\/code>, <code>rest\/rcs\/v1\/inbox<\/code> and <code>rest\/tg\/v1\/inbox<\/code>, 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. <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/receiving-messages-four-channel-inbox-contracts\/\">Receiving messages on four channels<\/a> has the inbox contracts.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"credential-rotation\" class=\"wp-block-heading\">Passwords and Credential Rotation for Child Accounts<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The reseller family offers two ways to deal with a child&#8217;s password.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th><\/th><th><code>generateuserpassword<\/code><\/th><th><code>resetuserpassword<\/code><\/th><\/tr><\/thead><tbody><tr><td>What happens<\/td><td>The child receives an email with a reset link<\/td><td>You set the new password directly<\/td><\/tr><tr><td>Extra parameter<\/td><td>None<\/td><td><code>newPassword<\/code><\/td><\/tr><tr><td>Who knows the new password<\/td><td>Only the child<\/td><td>You, and anything that logged the request<\/td><\/tr><tr><td>Success <code>msg<\/code><\/td><td>&#8220;Password changed successfully.&#8221;<\/td><td>&#8220;Password changed successfully.&#8221;<\/td><\/tr><tr><td>Right for<\/td><td>A human who signs in to the panel<\/td><td>A service account you operate entirely<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>For children that a client&#8217;s staff use, prefer <code>generateuserpassword<\/code>.<\/strong> You never hold the password, and the email goes to the address on the child&#8217;s profile. Read the profile first and confirm <code>emailId<\/code> is the address you expect; a reset link sent to a stale address is an account handover.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>For service children your platform operates, prefer API keys over passwords.<\/strong> A password is needed for panel access; your sending code should authenticate with the child&#8217;s API key in the <code>apikey<\/code> header plus <code>userid<\/code>. If you do set a password with <code>resetuserpassword<\/code>, 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 <code>newPassword<\/code> in clear text.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Rotate keys per account, never globally.<\/strong> In a child-account layout, rotating Brand A&#8217;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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For team sub-users, password resets are done from Sub-Users &gt; Manage in the panel, alongside activate, deactivate and email and name updates.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"code-samples\" class=\"wp-block-heading\">Four Code Samples, Four Traps<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Each sample shows one trap from this guide. They share the client shape used across the API: parse loosely, read only the top-level <code>status<\/code>, return on failure, and bind typed fields only on success.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Python: reading a child account without mis-dating its expiry<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The trap: quoted epoch milliseconds formatted in UTC show the day before the expiry you set.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import requests\nfrom dataclasses import dataclass\nfrom datetime import datetime, timezone, timedelta\n\nBASE = \"https:\/\/unify.smsgateway.center\/SMSApi\/reseller\/\"\nIST = timezone(timedelta(hours=5, minutes=30))\n\n@dataclass(frozen=True)\nclass ChildAccount:\n    user_id: str          # quoted digits, kept as text\n    login: str\n    user_type: str        # normalised to lower case\n    status: str\n    expires_ist: datetime\n    sms_balance: int\n    postal_city: str      # a NAME on readuser, unlike readprofile\n\ndef epoch_ms_ist(value: str) -&gt; datetime:\n    return datetime.fromtimestamp(int(value) \/ 1000, tz=IST)\n\ndef read_child(parent_login: str, api_key: str, child_login: str) -&gt; ChildAccount | None:\n    r = requests.post(\n        BASE + \"readuser\",\n        headers={\"apikey\": api_key},\n        data={\"userid\": parent_login, \"userloginname\": child_login, \"output\": \"json\"},\n        timeout=15,\n    )\n    body = r.json().get(\"response\", {})\n    if body.get(\"status\") != \"success\":\n        return None\n    rows = body.get(\"userList\") or &#91;]\n    if not rows:\n        return None\n    u = rows&#91;0].get(\"user\", {})\n    return ChildAccount(\n        user_id=str(u&#91;\"userId\"]),\n        login=u&#91;\"userName\"],\n        user_type=u.get(\"userType\", \"\").lower(),\n        status=u.get(\"userStatus\", \"\"),\n        expires_ist=epoch_ms_ist(u&#91;\"expDate\"]),\n        sms_balance=int(u.get(\"smsBalance\", \"0\")),\n        postal_city=u.get(\"postalCity\", \"\"),\n    )\n\n# \"1709231400000\" -&gt; 2024-03-01 00:00 IST. In UTC it would print as 2024-02-29.<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Node.js: an idempotent credit transfer<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The trap: <code>addcredit<\/code> returns no id and no balance, so a blind retry can credit a child twice.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import crypto from \"node:crypto\";\n\nconst BASE = \"https:\/\/unify.smsgateway.center\/SMSApi\/reseller\/\";\n\nasync function post(action, apiKey, fields) {\n  const body = new URLSearchParams(fields);            \/\/ never hand-build the body\n  const res = await fetch(BASE + action, {\n    method: \"POST\",\n    headers: { apikey: apiKey, \"Content-Type\": \"application\/x-www-form-urlencoded\" },\n    body,\n    signal: AbortSignal.timeout(15000),\n  });\n  return (await res.json()).response ?? {};\n}\n\nfunction istDate(d = new Date()) {\n  return new Date(d.getTime() + 330 * 60000).toISOString().slice(0, 10);\n}\n\nasync function findTransfer(parent, apiKey, child, ref) {\n  const r = await post(\"readcredithistory\", apiKey, {\n    userid: parent, userloginname: child,\n    fromdate: istDate(new Date(Date.now() - 86400000)), todate: istDate(), output: \"json\",\n  });\n  if (r.status !== \"success\") return null;\n  return (r.historyList ?? &#91;]).map(x =&gt; x.history ?? {})\n    .find(h =&gt; String(h.comments ?? \"\").startsWith(ref)) ?? null;\n}\n\nexport async function transferCredits(db, parent, apiKey, child, credits, note) {\n  const ref = \"ct-\" + crypto.randomBytes(4).toString(\"hex\");\n  await db.insertTransfer({ ref, child, credits, state: \"PENDING\" });   \/\/ commit first\n\n  let state = \"UNKNOWN\";\n  try {\n    const r = await post(\"addcredit\", apiKey, {\n      userid: parent, userloginname: child, product: \"SMS\",\n      transactiontype: \"purchase\", type: \"purchase\",      \/\/ both spellings, same value\n      credits: String(credits), comment: `${ref} ${note}`, output: \"json\",\n    });\n    state = r.status === \"success\" ? \"SENT\" : \"ERROR\";\n  } catch { \/* timeout or network: state stays UNKNOWN *\/ }\n\n  const row = await findTransfer(parent, apiKey, child, ref);\n  if (row) {\n    await db.updateTransfer(ref, { state: \"CONFIRMED\", historyId: String(row.uuId), balance: row.balance });\n  } else {\n    await db.updateTransfer(ref, { state: state === \"ERROR\" ? \"FAILED\" : \"UNKNOWN\" });\n    \/\/ UNKNOWN is re-read later by a recovery job. It is never resent from here.\n  }\n  return ref;\n}<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Java: a resolver that fails closed<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The trap: falling back to an account default sends Brand B&#8217;s message under Brand A&#8217;s sender, entity or bot.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public record SendContext(String brandKey, String channel, String loginName,\n                          String secretRef, String senderKey, String dltEntityId,\n                          String templateRef) {}\n\npublic final class BrandResolver {\n    private final BrandRegistry registry;\n\n    public BrandResolver(BrandRegistry registry) { this.registry = registry; }\n\n    public SendContext resolve(String brandKey, String channel, String templateKey) {\n        BrandChannel bc = registry.channel(brandKey, channel)\n            .filter(BrandChannel::enabled)\n            .orElseThrow(() -&gt; new MisconfiguredBrand(brandKey + \" has no enabled \" + channel));\n\n        BrandTemplate t = registry.template(brandKey, channel, templateKey)\n            .orElseThrow(() -&gt; new MisconfiguredBrand(brandKey + \" has no template \" + templateKey));\n\n        if (t.senderKey() != null &amp;&amp; !t.senderKey().equals(bc.senderKey())) {\n            throw new MisconfiguredBrand(\"template \" + templateKey\n                + \" is registered for \" + t.senderKey() + \", brand sends as \" + bc.senderKey());\n        }\n        if (\"sms\".equals(channel) &amp;&amp; (bc.dltEntityId() == null || bc.dltEntityId().isBlank())) {\n            throw new MisconfiguredBrand(brandKey + \" has no DLT entity id\");\n        }\n\n        MessagingAccount acct = registry.account(bc.accountId());\n        if (!\"active\".equalsIgnoreCase(acct.status())) {\n            throw new MisconfiguredBrand(\"account \" + acct.loginName() + \" is \" + acct.status());\n        }\n        return new SendContext(brandKey, channel, acct.loginName(), acct.secretRef(),\n                               bc.senderKey(), bc.dltEntityId(), t.platformRef());\n    }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">No method on <code>BrandResolver<\/code> returns a default. Every miss is an exception, and every exception is a configuration alert, not a retry.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">PHP: a delivery report receiver that knows its account before it parses<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The trap: one webhook per account means a shared URL cannot tell accounts apart, and a body-first parser guesses.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;?php\n\/\/ Route: POST or GET \/sms-dlr\/{pathKey}\/\nfunction handleDlr(PDO $db, string $pathKey): void\n{\n    $stmt = $db-&gt;prepare('SELECT account_id FROM messaging_account WHERE webhook_path_key = ?');\n    $stmt-&gt;execute(&#91;$pathKey]);\n    $accountId = $stmt-&gt;fetchColumn();\n    if ($accountId === false) {\n        http_response_code(404);            \/\/ unknown token: reveal nothing\n        return;\n    }\n\n    $raw = file_get_contents('php:\/\/input');\n    if ($raw === '' || $raw === false) {\n        $raw = http_build_query($_GET);     \/\/ accept either delivery style\n    }\n\n    $ins = $db-&gt;prepare(\n        'INSERT INTO dlr_inbox (account_id, received_at, raw_body) VALUES (?, NOW(), ?)'\n    );\n    $ins-&gt;execute(&#91;$accountId, $raw]);      \/\/ persist raw, parse later\n\n    http_response_code(200);\n    echo 'OK';\n}\n\/\/ A worker extracts the transaction id as a STRING, joins to outbound_message\n\/\/ for brand_id, and reconciles with SMSApi\/reports\/status method=getDlr.<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"migration-checklist\" class=\"wp-block-heading\">Migration Checklist<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Moving from &#8220;one account, one brand&#8221; to a multi-brand layout:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>List every brand, its legal entity, budget owner and channels.<\/li>\n\n\n\n<li>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.<\/li>\n\n\n\n<li>Create <code>messaging_account<\/code>, <code>brand<\/code>, <code>brand_channel<\/code> and <code>brand_template<\/code>, with the Telegram partial unique index.<\/li>\n\n\n\n<li>Add <code>brand_id<\/code> to the outbound message table and backfill existing rows.<\/li>\n\n\n\n<li>Prefix template identifiers and WhatsApp template names with the brand key.<\/li>\n\n\n\n<li>Route every send through the resolver; delete code that reads account-level defaults.<\/li>\n\n\n\n<li>Pass <code>dltEntityId<\/code> and <code>dltTemplateId<\/code> on every SMS send.<\/li>\n\n\n\n<li>For each child: <code>createuser<\/code> with a form encoder, <code>readuser<\/code>, verify every field, store <code>userId<\/code> as text.<\/li>\n\n\n\n<li>Convert <code>expDate<\/code>, <code>regDate<\/code> and history <code>timestamp<\/code> in IST everywhere they are displayed or compared.<\/li>\n\n\n\n<li>Build the <code>credit_transfer<\/code> table and the three-step transfer; never retry a transfer without reading history first.<\/li>\n\n\n\n<li>Verify the adjustment spelling with a one-credit test before the first production adjustment.<\/li>\n\n\n\n<li>Snapshot every child&#8217;s <code>smsBalance<\/code> daily from the first day.<\/li>\n\n\n\n<li>Register a distinct webhook path token per account.<\/li>\n\n\n\n<li>Map RCS bot names and bot ids, and normalised WhatsApp numbers, to brands; keep inactive bots in the registry.<\/li>\n\n\n\n<li>Store each Telegram <code>botToken<\/code> in the secret store keyed by account.<\/li>\n\n\n\n<li>Use <code>generateuserpassword<\/code> for human children; keep <code>newPassword<\/code> out of all logs.<\/li>\n\n\n\n<li>Add a nightly drift check on the reseller endpoints, as described in <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/contract-testing-harness-messaging-api\/\">the contract testing harness<\/a>.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"unspecified-behaviour\" class=\"wp-block-heading\">Unspecified Behaviour and How to Code Around It<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>One. Treat a team sub-user&#8217;s sender IDs, templates, WhatsApp numbers and wallet as shared with the main account.<\/strong> Sub-users get the main account&#8217;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&#8217;s staff can use another brand&#8217;s sender.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Two. Confirm every credit transfer by reference, never by message.<\/strong> Whether <code>addcredit<\/code> 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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Three. Send both <code>transactiontype<\/code> and <code>type<\/code>, and both <code>mobileno<\/code> and <code>mobileNo<\/code>, with identical values.<\/strong> A server reads at most one key of each pair. Duplicated keys with the same value are correct under either spelling.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Four. Pin the adjustment spelling from observation.<\/strong> Move one credit with <code>adjustments<\/code>, read the history row&#8217;s <code>method<\/code>, and store whichever value the platform recorded. Until that check passes, route adjustments through a human.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Five. Send <code>city<\/code>, <code>region<\/code> and <code>country<\/code>, then verify with <code>readuser<\/code>.<\/strong> The meaning of <code>region<\/code> differs between the create table and its sample. The read-back fields <code>postalCity<\/code>, <code>postalRegion<\/code> and <code>postalCountry<\/code> tell you which input landed where, so one provisioning run settles it for good.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Six. Treat <code>expirydate<\/code> as the first instant of that day in IST.<\/strong> The sample expiry is exactly midnight IST. If a contract says &#8220;valid through&#8221; a date, send the following day. Being a day generous is recoverable; cutting off a client a day early is not.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Seven. Verify the parent&#8217;s balance moves on every batch of transfers.<\/strong> &#8220;Credit transferred&#8221; implies the parent is debited. Snapshot the parent&#8217;s <code>smsBalance<\/code> before and after; a mismatch is found on day one rather than at month end.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Eight. Assume the comment may be truncated, and put the reference first.<\/strong> A short reference at the start of <code>comment<\/code> survives any plausible length limit. Match with a prefix test, not equality.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Nine. Read each child by login name, and keep your own list of children.<\/strong> Whether <code>readuser<\/code> without a login name returns every child is unclear from its required parameters. Your <code>messaging_account<\/code> table is the list; the platform confirms each entry.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Ten. Treat any <code>userStatus<\/code> other than an observed &#8220;Active&#8221; as not sendable.<\/strong> Only <code>\"Active\"<\/code> appears in the sample. An allowlist of observed values fails closed: an unknown status pauses a brand instead of sending into a suspended account.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Eleven. Budget WhatsApp and RCS per brand in your own ledger.<\/strong> Credit transfers cover <code>SMS<\/code>, <code>Voice<\/code> and <code>Miss Call<\/code>. Whether a child&#8217;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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Twelve. Keep a second Telegram brand out of any account that already has one.<\/strong> 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&#8217;s bot.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"faqs\" class=\"wp-block-heading\">FAQs<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Can one SMSGatewayCenter account send for several brands?<\/strong><br>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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>What is the difference between a sub-user and a reseller child account?<\/strong><br>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 <code>SMSApi\/reseller\/createuser<\/code>, with its own balance, rate plan, login and channel registrations.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Can a sub-user read the rate plan?<\/strong><br>No. <code>SMSApi\/rateplan\/read<\/code> returns code <code>486<\/code>, &#8220;Rate plans for sub-users are undefined. Please review the rates in the main account.&#8221; Sub-users inherit pricing from the main account.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Can a sub-user create more sub-users?<\/strong><br>No. Sub-users cannot create further sub-users, and the sub-user feature is available on direct customer accounts, not reseller accounts.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Does <code>createuser<\/code> return the new user&#8217;s id?<\/strong><br>No. The success response contains only <code>status<\/code>, <code>msg<\/code> and <code>code<\/code>. Call <code>readuser<\/code> with the same <code>userloginname<\/code> and store the quoted <code>userId<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Why does <code>readuser<\/code> show an expiry one day before the date I set?<\/strong><br>You are probably formatting it in UTC. <code>expDate<\/code> is epoch milliseconds for midnight IST on the chosen date; in UTC that is 18:30 on the previous day. Convert in <code>Asia\/Kolkata<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>How do I avoid crediting a child account twice?<\/strong><br>Write an intent row with a unique reference, put the reference at the start of <code>comment<\/code>, and confirm the transfer by finding that reference in <code>readcredithistory<\/code> before any retry.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Can I transfer WhatsApp or RCS credits to a child account?<\/strong><br>The <code>product<\/code> values for <code>addcredit<\/code> and <code>removecredit<\/code> are <code>SMS<\/code>, <code>Voice<\/code> and <code>Miss Call<\/code>. Meter WhatsApp and RCS per brand in your own ledger and confirm a child&#8217;s RCS rate plan when RCS is enabled for that user.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Should reseller calls use GET or POST?<\/strong><br>POST. All eight endpoints accept it, and credit movements should never travel in a URL.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Why does <code>postalCity<\/code> contain a number on one endpoint and a name on another?<\/strong><br><code>SMSApi\/account\/readprofile<\/code> returns identifiers such as <code>\"2707\"<\/code> in <code>postalCity<\/code>, while <code>SMSApi\/reseller\/readuser<\/code> returns names such as <code>\"Mumbai\"<\/code>. Map the two payloads with separate types.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Can two brands share one Telegram bot?<\/strong><br>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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>How do I route SMS delivery reports when several brands share an account?<\/strong><br>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 <code>brand_id<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Should I give each client its own child account or let them connect their own?<\/strong><br>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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Can a reseller add sender IDs for a child account?<\/strong><br>Yes, in the white-label panel under &#8220;User Sender Names&#8221;. Through the API, call the <code>SMSApi\/senderid\/<\/code> endpoints with the child&#8217;s own credentials.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">Lets Start Multi-Brand Messaging<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Running several brands, clients or business units through one messaging stack? Start with a free sandbox account on the <a href=\"https:\/\/www.smsgatewaycenter.com\/demo\/\">SMSGatewayCenter demo page<\/a> to test sender IDs, templates and delivery reports, or look at the <a href=\"https:\/\/www.smsgatewaycenter.com\/bulk-sms-reseller-gateway\/\">bulk SMS reseller programme<\/a> if you need child accounts with their own balances. For a layout review with our team, <a href=\"https:\/\/www.smsgatewaycenter.com\/contact\/\">get in touch<\/a>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">Latest Posts<\/h3>\n\n\n<ul class=\"wp-block-latest-posts__list is-grid columns-3 wp-block-latest-posts is-layout-grid wp-container-core-latest-posts-is-layout-0fed8f92 wp-block-latest-posts-is-layout-grid\"><li><a class=\"wp-block-latest-posts__post-title\" href=\"https:\/\/www.smsgatewaycenter.com\/blog\/multi-brand-messaging-architecture-accounts-sub-users\/\">Multi-Brand Messaging Architecture: Accounts, Sub-Users and Reseller Child Accounts for SMS, WhatsApp, RCS and Telegram<\/a><\/li>\n<li><a class=\"wp-block-latest-posts__post-title\" href=\"https:\/\/www.smsgatewaycenter.com\/blog\/contract-testing-harness-messaging-api\/\">Contract Testing a Messaging API: A Nightly Drift Harness That Never Sends a Message<\/a><\/li>\n<li><a class=\"wp-block-latest-posts__post-title\" href=\"https:\/\/www.smsgatewaycenter.com\/blog\/sender-identity-sms-whatsapp-rcs-telegram\/\">Sender Identity Across SMS, WhatsApp, RCS and Telegram: Sender IDs, WABA Numbers, Bots and Long Codes<\/a><\/li>\n<li><a class=\"wp-block-latest-posts__post-title\" href=\"https:\/\/www.smsgatewaycenter.com\/blog\/message-template-management-four-channels\/\">Message Template Management Across SMS, RCS, WhatsApp and Telegram: One API Comparison<\/a><\/li>\n<li><a class=\"wp-block-latest-posts__post-title\" href=\"https:\/\/www.smsgatewaycenter.com\/blog\/reconciling-messaging-invoice-four-channels\/\">Reconciling a Messaging Invoice Across Four Channels: Credits, Currency and the Rate Plan API<\/a><\/li>\n<\/ul>\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n","protected":false},"excerpt":{"rendered":"<p>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.<\/p>\n","protected":false},"author":118,"featured_media":3056,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2010],"tags":[2291,404,2290,2294,2234,2292,2090,481,2293,2252,632],"class_list":["post-3055","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-developer-guides","tag-credit-management","tag-dlt","tag-multi-brand-messaging","tag-multi-tenant-architecture","tag-rcs","tag-reseller-api","tag-sender-id","tag-sms-api","tag-sub-users","tag-telegram","tag-whatsapp-business-api"],"_links":{"self":[{"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/posts\/3055","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/users\/118"}],"replies":[{"embeddable":true,"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/comments?post=3055"}],"version-history":[{"count":0,"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/posts\/3055\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/media\/3056"}],"wp:attachment":[{"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/media?parent=3055"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/categories?post=3055"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/tags?post=3055"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}