{"id":2944,"date":"2026-09-04T11:11:22","date_gmt":"2026-09-04T05:41:22","guid":{"rendered":"https:\/\/www.smsgatewaycenter.com\/blog\/?p=2944"},"modified":"2026-09-04T11:11:24","modified_gmt":"2026-09-04T05:41:24","slug":"campaign-splitting-one-send-several-transactions","status":"publish","type":"post","link":"https:\/\/www.smsgatewaycenter.com\/blog\/campaign-splitting-one-send-several-transactions\/","title":{"rendered":"Campaign Splitting: What Happens When One Send Becomes Several Transactions"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">A split SMS campaign turns one send into several transactions, each with its own identifier. Here is what the campaign, transaction and message levels actually mean, which SMSGatewayCenter endpoint resolves each one, and how to aggregate and reconcile a campaign without double counting or reporting a false delivery rate.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/campaign-splitting-one-send-several-transactions.webp\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"584\" src=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/campaign-splitting-one-send-several-transactions-1024x584.webp\" alt=\"Diagram of one campaign node fanning out into three timed batch nodes which each fan out into many individual message nodes.\" class=\"wp-image-2945\" srcset=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/campaign-splitting-one-send-several-transactions-1024x584.webp 1024w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/campaign-splitting-one-send-several-transactions-300x171.webp 300w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/campaign-splitting-one-send-several-transactions-768x438.webp 768w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/campaign-splitting-one-send-several-transactions.webp 1200w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><figcaption class=\"wp-element-caption\">One campaign, several transactions, many messages. Splitting changes the shape of your identifiers, not just the timing of your sends.<\/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=\"#what-splitting-is\">What a Split Campaign Actually Is<\/a><\/li>\n\n\n\n<li><a href=\"#three-identity-levels\">The Three Levels of Identity<\/a><\/li>\n\n\n\n<li><a href=\"#api-capability-gap\">What the API Can and Cannot Do With a Split<\/a><\/li>\n\n\n\n<li><a href=\"#reading-a-campaign\">Reading a Campaign<\/a><\/li>\n\n\n\n<li><a href=\"#send-response-union\">The Send Response Is a Union, Not a Shape<\/a><\/li>\n\n\n\n<li><a href=\"#group-send-blind-spot\">The Group Send Blind Spot<\/a><\/li>\n\n\n\n<li><a href=\"#update-and-delete\">Updating and Deleting a Split Campaign<\/a><\/li>\n\n\n\n<li><a href=\"#schedule-time-spellings\">Three Spellings of One Concept<\/a><\/li>\n\n\n\n<li><a href=\"#aggregating-correctly\">Aggregating a Split Campaign Correctly<\/a><\/li>\n\n\n\n<li><a href=\"#reporting-resolution\">Which Report Resolves Which Level<\/a><\/li>\n\n\n\n<li><a href=\"#schema-changes\">Schema Changes You Need<\/a><\/li>\n\n\n\n<li><a href=\"#reconciliation-playbook\">The Reconciliation Playbook<\/a><\/li>\n\n\n\n<li><a href=\"#billing-under-split\">Billing and Cost Under a Split<\/a><\/li>\n\n\n\n<li><a href=\"#decision-matrix\">Decision Matrix<\/a><\/li>\n\n\n\n<li><a href=\"#build-order\">Build Order<\/a><\/li>\n\n\n\n<li><a href=\"#ten-mistakes\">Ten Mistakes<\/a><\/li>\n\n\n\n<li><a href=\"#checklist\">Pre-Launch Checklist<\/a><\/li>\n\n\n\n<li><a href=\"#not-claimed\">What This Article Deliberately Does Not Claim<\/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 you enable split campaigns, one logical send stops being one transaction and becomes several, each with its own transaction identifier, each firing at a different time, and only the first of which your code ever observes synchronously. The identifier that ties them together is <code>campaignid<\/code>, and it is the only key that addresses the campaign as a whole. If your database has one row per send and one status column keyed on the transaction identifier you received at submit time, that model is wrong the moment splitting is switched on, because the value you stored describes one batch out of several and the rest of the campaign will never call you back.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The practical consequence is not a crash. It is silence. Your integration keeps working, your first batch reports normally, and your delivery rate looks catastrophic for hours because the denominator you are dividing by is the campaign total while the numerator only covers the batches that have actually fired. Nothing errors. The numbers are just wrong, and they correct themselves later in a way that hides the bug.<\/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>A split campaign is configured in the portal, not through the send API. There is no documented parameter on <code>SMSApi\/send<\/code> that creates one. Your code cannot opt in or out, so it must be correct either way.<\/li>\n\n\n\n<li>Identity is three levels deep: <code>campaignid<\/code> addresses the campaign, <code>transactionId<\/code> (returned as <code>uuId<\/code> in reports) addresses one batch, and <code>msgId<\/code> addresses one physical message to one recipient. Only <code>msgId<\/code> is safely unique per row.<\/li>\n\n\n\n<li><code>SMSApi\/campaign\/read<\/code> is the only endpoint that enumerates the batches of a campaign. It is documented verbatim as fetching &#8220;details of split campaign&#8221;.<\/li>\n\n\n\n<li><code>SMSApi\/campaign\/update<\/code> accepts exactly one mutable field, <code>scheduletime<\/code>. You can move a campaign in time. You cannot change its batch size, its interval, its body or its recipients through the API.<\/li>\n\n\n\n<li><code>SMSApi\/campaign\/delete<\/code> is keyed on <code>campaignid<\/code>, so it addresses the whole remaining campaign. There is no documented way to delete a single pending batch through the API.<\/li>\n\n\n\n<li>The send response shape varies by <code>sendMethod<\/code>, and there is no discriminator field inside the response telling you which shape you got. A group send returns <code>group<\/code> and no recipient information at all.<\/li>\n\n\n\n<li>Provider identifiers run to nineteen digits and exceed the largest integer JavaScript can represent exactly. Store every one of them as text.<\/li>\n\n\n\n<li>Delivery rate must be computed against submitted batches, never against the campaign total, until the last batch has fired.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"what-splitting-is\" class=\"wp-block-heading\">What a Split Campaign Actually Is<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Split campaigns are a portal feature. On the <a href=\"https:\/\/www.smsgatewaycenter.com\/split-sms-campaigns\/\">Split SMS Campaigns product page<\/a> the mechanism is described plainly: you upload a large recipient file, you toggle &#8220;Enable Split Campaign&#8221;, you set a batch limit, and you choose an interval from a dropdown. The documentation states that the first batch &#8220;will deliver instantly or at scheduled time&#8221; and each subsequent batch fires after the chosen interval has elapsed.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That is worth restating in engineering terms, because the marketing framing hides the part that matters. Splitting is a server-side scheduler that takes one recipient list and emits several submissions on a timer. It is not client-side chunking. You are not making several API calls. You make zero additional API calls, and the platform makes the additional submissions on your behalf, hours or days after your process has exited.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The portal supports splitting across all three send methods: comma-separated numbers in a simple send, one or more groups in a <a href=\"https:\/\/www.smsgatewaycenter.com\/developer-api\/send-sms-group\/\">group send<\/a>, and every variety of file upload. Three quite different submission shapes, one splitting behaviour.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The reason teams reach for it is bounded human capacity rather than gateway throughput. If fifty thousand people receive a promotional offer at the same instant, the inbound calls land at the same instant too. Spreading the send spreads the callbacks. That is a legitimate reason, and it is a different reason from the ones that motivate <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/rate-limiting-backpressure-messaging-systems\/\">rate limiting and backpressure<\/a>, which is about protecting the gateway rather than protecting your sales desk. The two techniques look similar from a distance and solve unrelated problems.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The part nobody documents in engineering terms<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Here is the asymmetry that makes this article necessary. The API can read a split campaign, reschedule a split campaign and delete a split campaign. The API cannot create one. Every capability you have is downstream of a decision somebody made in a browser.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That means the split is not a property of your request. It is a property of the account or the campaign configuration, invisible in your request and invisible in your immediate response, and it changes the number of transactions your one logical send produces. An integration that assumes one send equals one transaction is not making a wrong assumption about the API. It is making a wrong assumption about who is in control.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"three-identity-levels\" class=\"wp-block-heading\">The Three Levels of Identity<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Most integrations against this platform, including several written on this blog, have modelled identity with two levels: the transaction you submitted and the messages inside it. That model is incomplete. There are three.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/diagram-campaign-identity-levels.webp\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"597\" src=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/diagram-campaign-identity-levels-1024x597.webp\" alt=\"Diagram of the three identity levels of a split SMS campaign, showing campaignid resolved by the campaign endpoints, transactionId and uuId resolved by the by-transaction report, and msgId resolved by the delivery report feed.\" class=\"wp-image-2947\" srcset=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/diagram-campaign-identity-levels-1024x597.webp 1024w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/diagram-campaign-identity-levels-300x175.webp 300w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/diagram-campaign-identity-levels-768x448.webp 768w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/diagram-campaign-identity-levels.webp 1200w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><figcaption class=\"wp-element-caption\">One campaignid fans out to many transaction identifiers, and each transaction identifier fans out to many message identifiers. Each level has exactly one endpoint that addresses it.<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Level one, the campaign.<\/strong> Identified by <code>campaignid<\/code>. This is the only identifier that means &#8220;the whole thing the user asked for&#8221;. It is accepted by <code>SMSApi\/campaign\/read<\/code>, <code>SMSApi\/campaign\/update<\/code> and <code>SMSApi\/campaign\/delete<\/code>, and by nothing else. It never appears in a send response.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Level two, the transaction.<\/strong> Identified by <code>transactionId<\/code> in a send response and by <code>uuId<\/code> in every report. Same value, two spellings, and the spelling changes depending on which side of the wire you are reading. One transaction corresponds to one submission, which under splitting means one batch. This is the level your existing code almost certainly conflates with the campaign.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Level three, the message.<\/strong> Identified by <code>msgId<\/code>. One row per recipient per send. This is the only identifier that is one to one with a physical SMS, and therefore the only safe primary key for a per-recipient table. The <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/outbound-message-table-schema-design\/\">outbound message table design<\/a> article treats this level in depth and remains the canonical schema reference; this article extends it upward by one level.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">All three are text, not numbers<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Every identifier on this platform is a long decimal string. The documented samples run to nineteen digits for transaction identifiers, and the campaign endpoint samples show seventeen digits for <code>campaignid<\/code>. Both exceed <code>Number.MAX_SAFE_INTEGER<\/code>, which is sixteen digits. Any language that parses JSON into IEEE 754 doubles by default will silently corrupt these values, and the corruption is subtle: the string looks almost right, differing only in the last two or three digits, so it passes eyeballing and fails lookups.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Node.js. This is data loss, and it does not throw.\nJSON.parse('{\"transactionId\": 8359251506264886974}').transactionId\n\/\/ 8359251506264887000\n\n\/\/ The values arrive quoted in the documented samples, which saves you\n\/\/ only until something in your pipeline normalises them.\nJSON.parse('{\"transactionId\": \"8359251506264886974\"}').transactionId\n\/\/ '8359251506264886974'   correct<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Column type <code>TEXT<\/code> or <code>VARCHAR<\/code>, never <code>BIGINT<\/code>, never <code>NUMERIC<\/code>. This is the same rule that applies at the message level, and splitting simply gives you one more identifier to get wrong.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"api-capability-gap\" class=\"wp-block-heading\">What the API Can and Cannot Do With a Split<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Operation<\/th><th>Endpoint<\/th><th>Available via API<\/th><th>Notes<\/th><\/tr><\/thead><tbody><tr><td>Create a split campaign<\/td><td>none<\/td><td>No<\/td><td>Configured in the portal only<\/td><\/tr><tr><td>Set batch size<\/td><td>none<\/td><td>No<\/td><td>Portal only<\/td><\/tr><tr><td>Set the interval between batches<\/td><td>none<\/td><td>No<\/td><td>Portal only<\/td><\/tr><tr><td>List the batches of a campaign<\/td><td><code>SMSApi\/campaign\/read<\/code><\/td><td>Yes<\/td><td>Requires <code>campaignid<\/code><\/td><\/tr><tr><td>Read one batch in detail<\/td><td><code>SMSApi\/reports\/status<\/code><\/td><td>Yes<\/td><td>Requires <code>uuid<\/code>, one batch per call<\/td><\/tr><tr><td>Reschedule the campaign<\/td><td><code>SMSApi\/campaign\/update<\/code><\/td><td>Yes<\/td><td>Only <code>scheduletime<\/code> is mutable<\/td><\/tr><tr><td>Change the message body<\/td><td>none<\/td><td>No<\/td><td>Not exposed<\/td><\/tr><tr><td>Change the recipient list<\/td><td>none<\/td><td>No<\/td><td>Not exposed<\/td><\/tr><tr><td>Delete one pending batch<\/td><td>none<\/td><td>No<\/td><td>Not exposed<\/td><\/tr><tr><td>Delete the whole campaign<\/td><td><code>SMSApi\/campaign\/delete<\/code><\/td><td>Yes<\/td><td>Requires <code>campaignid<\/code><\/td><\/tr><tr><td>Discover that splitting is on<\/td><td>none<\/td><td>No<\/td><td>Infer it from the batch count<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">That last row deserves emphasis. There is no flag anywhere in a send response that tells you a split is in force. You discover it by calling <code>campaign\/read<\/code> and observing more than one entry, which requires you to already hold a <code>campaignid<\/code>, which the send response does not give you. In a pure API integration where nobody has visited the portal, splitting is not something you detect. It is something you are told about by a colleague, or something you deduce from a delivery curve with steps in it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The correct posture is therefore defensive rather than conditional. Do not write code that branches on whether splitting is enabled. Write code that is correct when a campaign has N batches, and let N equal one in the ordinary case.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"reading-a-campaign\" class=\"wp-block-heading\">Reading a Campaign<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><code>SMSApi\/campaign\/read<\/code> accepts POST or GET. The documentation describes the <code>campaignid<\/code> parameter verbatim as &#8220;Enter the campaign ID to fetch details of split campaign&#8221;, which is the platform stating in its own words that campaign and split campaign are the same object.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>curl -X POST \"https:\/\/unify.smsgateway.center\/SMSApi\/campaign\/read\" \\\n  -H \"apikey: YOUR_API_KEY\" \\\n  -H \"Content-Type: application\/x-www-form-urlencoded\" \\\n  --data-urlencode \"userid=YOUR_USER_ID\" \\\n  --data-urlencode \"campaignid=83147103413249843\" \\\n  --data-urlencode \"output=json\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The response wraps its payload in the standard envelope and returns a list:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"response\": {\n    \"api\": \"campaign\",\n    \"action\": \"read\",\n    \"status\": \"success\",\n    \"msg\": \"success\",\n    \"code\": \"200\",\n    \"count\": 1,\n    \"campaignList\": &#91;\n      {\n        \"campaign\": {\n          \"uuId\": \"5718742519829975000\",\n          \"status\": \"pending\",\n          \"total\": \"1\",\n          \"timestamp\": \"1562974594713\",\n          \"scheduledTimestamp\": \"1564529760000\",\n          \"lastupdatedTimestamp\": \"0\"\n        }\n      }\n    ]\n  }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Four things in that shape will bite you.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The list is wrapped twice.<\/strong> <code>campaignList<\/code> is an array whose elements are objects keyed <code>campaign<\/code>, so a batch lives at <code>response.campaignList[i].campaign<\/code>. This double wrapping is consistent across the platform&#8217;s list endpoints and is easy to miss when you are writing the parser from the parameter table rather than from the sample.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>total<\/code> is a quoted string.<\/strong> So are <code>timestamp<\/code>, <code>scheduledTimestamp<\/code> and <code>lastupdatedTimestamp<\/code>. Coerce on ingest, once, at the boundary. Do not scatter <code>parseInt<\/code> through your reporting layer.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>lastupdatedTimestamp<\/code> of <code>\"0\"<\/code> means never.<\/strong> It is not an epoch of 1 January 1970. It is the platform&#8217;s sentinel for absence, the same convention that produces a <code>transactionId<\/code> of <code>\"0\"<\/code> on the OTP endpoints and an empty <code>refresh_token<\/code> on an OAuth refresh. Normalise sentinels to null at the boundary, or your &#8220;last updated&#8221; column will report a date in 1970 for every campaign that has never been touched.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>status<\/code> is an open enumeration.<\/strong> The sample shows <code>pending<\/code>. Treat any unrecognised value as unknown and surface it rather than mapping it into a bucket you have already defined. A closed enum here is a future incident.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>def parse_campaign_batches(payload):\n    \"\"\"Flatten the double-wrapped campaign list into plain batch records.\"\"\"\n    resp = payload&#91;\"response\"]\n    if resp.get(\"status\") != \"success\":\n        raise RuntimeError(f\"campaign\/read failed: {resp.get('msg')} ({resp.get('code')})\")\n\n    batches = &#91;]\n    for wrapper in resp.get(\"campaignList\", &#91;]):\n        c = wrapper&#91;\"campaign\"]\n        batches.append({\n            \"uu_id\": str(c&#91;\"uuId\"]),                 # text, always\n            \"status\": c.get(\"status\"),                # open enumeration\n            \"total\": int(c&#91;\"total\"]),                 # quoted string on the wire\n            \"created_ms\": _epoch(c.get(\"timestamp\")),\n            \"scheduled_ms\": _epoch(c.get(\"scheduledTimestamp\")),\n            \"updated_ms\": _epoch(c.get(\"lastupdatedTimestamp\")),\n        })\n    return batches\n\n\ndef _epoch(value):\n    \"\"\"Millisecond epoch as a quoted string, with '0' meaning never.\"\"\"\n    if value in (None, \"\", \"0\", 0):\n        return None\n    return int(value)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Note what <code>campaign\/read<\/code> does not give you: no message body, no sender identifier, no recipient list, no cost. It is a manifest of batches and nothing more. To learn anything about the content of a batch you must go to the report endpoints with the <code>uuId<\/code> you just extracted.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"send-response-union\" class=\"wp-block-heading\">The Send Response Is a Union, Not a Shape<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This is the finding that most often turns a working integration into a broken one, and splitting makes it worse because it multiplies the number of responses in play.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">There is one send endpoint, <code>https:\/\/unify.smsgateway.center\/SMSApi\/send<\/code>, and its response body is not one shape. It is a union whose member is selected by the <code>sendMethod<\/code> you passed, and the response contains no field identifying which member you received.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A quick send to comma-separated numbers returns:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"status\": \"success\",\n  \"mobile\": \"919999999999\",\n  \"invalidMobile\": \"\",\n  \"transactionId\": \"6305583318236810379\",\n  \"statusCode\": \"200\",\n  \"reason\": \"success\"\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">A group send returns:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"status\": \"success\",\n  \"group\": \"63\",\n  \"transactionId\": \"8359251506264886974\",\n  \"statusCode\": \"200\",\n  \"reason\": \"success\"\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">And the send sample on the authentication documentation, using a Basic Auth session token, returns a third key set including a <code>msgId<\/code> field and an eighteen-digit transaction identifier.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Three documented shapes, one endpoint. The only field common to all three is <code>transactionId<\/code>, and even that is absent from the OTP path, where it is documented as the literal string <code>\"0\"<\/code> and carries no information at all, a hazard covered in the <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/otp-delivery-india-reliability-checklist\/\">OTP delivery reliability checklist<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The engineering rule follows directly. Parse the send response against the <code>sendMethod<\/code> you sent, not against a single struct. Treat every field except <code>status<\/code>, <code>statusCode<\/code> and <code>transactionId<\/code> as optional. If you are using a statically typed language, model this as a sum type keyed on the request, and if you are using a dynamically typed one, resist the temptation to write <code>response.mobile<\/code> anywhere that a group send can reach.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>def parse_send_response(body, send_method):\n    common = {\n        \"status\": body&#91;\"status\"],\n        \"status_code\": str(body&#91;\"statusCode\"]),\n        \"transaction_id\": str(body.get(\"transactionId\", \"\")) or None,\n        \"reason\": body.get(\"reason\"),\n    }\n    if send_method == \"quick\":\n        common&#91;\"accepted_mobiles\"] = _csv(body.get(\"mobile\"))\n        common&#91;\"invalid_mobiles\"] = _csv(body.get(\"invalidMobile\"))\n    elif send_method == \"group\":\n        # No recipient information is returned. This is not an error.\n        common&#91;\"group_ids\"] = _csv(body.get(\"group\"))\n        common&#91;\"accepted_mobiles\"] = None\n        common&#91;\"invalid_mobiles\"] = None\n    return common<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"group-send-blind-spot\" class=\"wp-block-heading\">The Group Send Blind Spot<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Look again at that group response. There is no <code>mobile<\/code>. There is no <code>invalidMobile<\/code>. There is a group identifier and a transaction identifier, and that is the entire extent of what the platform tells you about who is going to receive this message.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For a quick send you at least learn which numbers were accepted and which were rejected, because <code>invalidMobile<\/code> enumerates the rejects while <code>status<\/code> can still read <code>success<\/code>. For a group send you learn nothing. The recipient list lives in the platform&#8217;s contact groups, it can change between the moment you compose the campaign and the moment a later batch fires, and your application never sees it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Combine that with splitting and the blind spot widens considerably:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>You submit a group send with splitting enabled.<\/li>\n\n\n\n<li>You receive one transaction identifier for batch one, and no recipient information.<\/li>\n\n\n\n<li>Batches two and three fire hours later, generating transaction identifiers you never observe, against a group whose membership may have changed since you composed the campaign.<\/li>\n\n\n\n<li>Your outbound message table has, at best, one row describing an intention. It cannot have per-recipient rows, because you were never told the recipients.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">There is exactly one way out, and it is the delivery report feed. The DLR feed returns <code>mobileNo<\/code>, <code>msgId<\/code>, <code>uuId<\/code>, <code>senderName<\/code>, <code>submitTime<\/code>, <code>deliveryTime<\/code>, <code>status<\/code> and <code>cause<\/code> per message. For a split group send, that feed is not a monitoring nicety. It is your only source of truth about what actually happened, and an integration that treats DLRs as optional telemetry has no record of its own sends. Set up the webhook as described in the <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/real-time-dlr-webhooks\/\">real-time DLR webhooks guide<\/a> and the <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/kb\/setup-webhook-sms-whatsapp-dlr\/\">webhook setup knowledge base entry<\/a>, and treat the feed as a system of record rather than as a dashboard input.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/diagram-split-submit-vs-reconcile.webp\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"649\" src=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/diagram-split-submit-vs-reconcile-1024x649.webp\" alt=\"Diagram contrasting what a client observes at submit time for a split campaign, where only the first batch returns a transaction identifier, with what must be fetched afterwards from the campaign read endpoint, the by-transaction report and the delivery report feed.\" class=\"wp-image-2946\" srcset=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/diagram-split-submit-vs-reconcile-1024x649.webp 1024w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/diagram-split-submit-vs-reconcile-300x190.webp 300w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/diagram-split-submit-vs-reconcile-768x486.webp 768w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/09\/diagram-split-submit-vs-reconcile.webp 1200w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><figcaption class=\"wp-element-caption\">Submit time gives you one batch. Everything else is reconstruction, and the delivery report feed is the only place recipients appear.<\/figcaption><\/figure>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"update-and-delete\" class=\"wp-block-heading\">Updating and Deleting a Split Campaign<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Both endpoints are keyed on <code>campaignid<\/code> and both are documented as POST.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Update<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>https:\/\/unify.smsgateway.center\/SMSApi\/campaign\/update<\/code> takes <code>campaignid<\/code> and <code>scheduletime<\/code>, in the format <code>YYYY-MM-DD HH:MM:SS<\/code>. The parameter descriptions are narrow and specific: <code>campaignid<\/code> is the &#8220;Identifier to update your campaign time&#8221;, and <code>scheduletime<\/code> is &#8220;Your new time to update the campaign time&#8221;.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>curl -X POST \"https:\/\/unify.smsgateway.center\/SMSApi\/campaign\/update\" \\\n  -H \"apikey: YOUR_API_KEY\" \\\n  -H \"Content-Type: application\/x-www-form-urlencoded\" \\\n  --data-urlencode \"userid=YOUR_USER_ID\" \\\n  --data-urlencode \"campaignid=83147103413249843\" \\\n  --data-urlencode \"scheduletime=2026-09-10 09:30:00\" \\\n  --data-urlencode \"output=json\"<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"response\": {\n    \"api\": \"campaign\",\n    \"action\": \"update\",\n    \"status\": \"success\",\n    \"msg\": \"Schedule time updated successfully.\",\n    \"code\": \"200\"\n  }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The introductory prose on that documentation page says the endpoint lets you &#8220;adjust details like timestamps or content&#8221;. The parameter table contains no content parameter. Trust the parameter table. Time is the only mutable dimension exposed here, and an integration built on the prose sentence will discover this the hard way.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Delete<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>https:\/\/unify.smsgateway.center\/SMSApi\/campaign\/delete<\/code> takes <code>campaignid<\/code> and <code>output<\/code>, and nothing else.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"response\": {\n    \"api\": \"campaign\",\n    \"action\": \"delete\",\n    \"status\": \"success\",\n    \"msg\": \"Schedule deleted successfully\",\n    \"code\": \"200\"\n  }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Two observations that matter more than they look.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The <code>msg<\/code> field says &#8220;Schedule&#8221;, not &#8220;Campaign&#8221;.<\/strong> So does the update response. The campaign family shares its human-readable strings with the schedule family. This is a reliable signal that campaigns and schedules are the same underlying object with two views onto it, and it is an absolute prohibition on parsing <code>msg<\/code>. Branch on <code>response.api<\/code> plus <code>response.action<\/code> plus <code>response.code<\/code>, which are stable and machine-oriented, and log <code>msg<\/code> for humans only.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Delete is campaign-wide.<\/strong> There is no <code>uuid<\/code> parameter on the delete endpoint. Whatever batches remain pending, all of them go. If a stakeholder asks you to cancel just the last two batches of a six-batch campaign, the API cannot do it. The <a href=\"https:\/\/www.smsgatewaycenter.com\/split-sms-campaigns\/\">Split SMS Campaigns page<\/a> states that already-sent batches cannot be undone and directs users to the portal or support for partial control, which is consistent with what the API exposes.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That asymmetry is worth designing around. If partial cancellation is a real requirement for your business, splitting is the wrong tool and you should chunk client-side instead, submitting each chunk as its own scheduled send so that each one is independently cancellable. You trade the platform&#8217;s convenience for granular control, and the <a href=\"#decision-matrix\">decision matrix<\/a> below sets out when that trade is worth making.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"schedule-time-spellings\" class=\"wp-block-heading\">Three Spellings of One Concept<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Scheduling appears at three points in this API and is spelled differently at each one.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Where<\/th><th>Parameter<\/th><th>Format<\/th><th>Notes<\/th><\/tr><\/thead><tbody><tr><td><code>SMSApi\/send<\/code>, all send methods<\/td><td><code>scheduleTime<\/code><\/td><td><code>YYYY-MM-DD HH:MM:SS<\/code><\/td><td>Camel case, with seconds<\/td><\/tr><tr><td><code>SMSApi\/campaign\/update<\/code><\/td><td><code>scheduletime<\/code><\/td><td><code>YYYY-MM-DD HH:MM:SS<\/code><\/td><td>All lower case, with seconds<\/td><\/tr><tr><td><code>WAApi\/send<\/code> (WhatsApp)<\/td><td><code>scheduletime<\/code><\/td><td><code>YYYY-MM-DD HH:MM<\/code><\/td><td>All lower case, no seconds<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Three spellings, two formats, one concept. There is no version boundary or product boundary that explains the split, and no timezone is stated for any of them.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The defence is a single serialisation helper per endpoint family rather than a shared one, plus a contract test that submits a scheduled send and reads it back to confirm the platform interpreted the string the way you meant. Do not build a global <code>format_schedule_time()<\/code> and use it everywhere; it will be right two times out of three and the third case will fail silently by scheduling to the top of the minute or by being rejected as an unknown parameter, which several endpoints on this platform do quietly rather than loudly.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This is the same class of problem as <code>userid<\/code> versus <code>userId<\/code>, which differ between the SMS API and the logout endpoint, and it belongs in the same place in your code: a thin, ugly, well-commented adapter layer at the boundary, not spread through your domain logic.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"aggregating-correctly\" class=\"wp-block-heading\">Aggregating a Split Campaign Correctly<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Here is the concrete failure. A campaign of sixty thousand recipients is split into six batches of ten thousand at three-hour intervals. Twenty minutes after the first batch fires, someone opens the dashboard.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The naive query divides delivered messages by the campaign total. Ten thousand submitted, eight thousand nine hundred delivered so far, denominator sixty thousand, and the dashboard says fourteen point eight percent delivered. Somebody escalates. An engineer starts checking sender identifiers and DLT template bindings. Nothing is wrong. Fifteen hours later the number is ninety-one percent and everyone forgets about it until the next campaign.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The fix is to make the denominator honest about time.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>-- Correct: rate against what has actually been submitted so far.\nSELECT\n    c.provider_campaign_id,\n    COUNT(*) FILTER (WHERE m.dlr_status = 'DELIVERED')          AS delivered,\n    COUNT(*) FILTER (WHERE m.dlr_status = 'FAILED')             AS failed,\n    COUNT(*) FILTER (WHERE m.dlr_status IS NULL)                AS awaiting_dlr,\n    COUNT(*)                                                     AS submitted,\n    ROUND(100.0 * COUNT(*) FILTER (WHERE m.dlr_status = 'DELIVERED')\n          \/ NULLIF(COUNT(*), 0), 2)                              AS delivered_pct_of_submitted,\n    b.batches_fired,\n    b.batches_total\nFROM outbound_message m\nJOIN campaign_batch    cb ON cb.uu_id = m.uu_id\nJOIN campaign          c  ON c.provider_campaign_id = cb.provider_campaign_id\nJOIN (\n    SELECT provider_campaign_id,\n           COUNT(*) FILTER (WHERE status &lt;&gt; 'pending') AS batches_fired,\n           COUNT(*)                                     AS batches_total\n    FROM campaign_batch\n    GROUP BY provider_campaign_id\n) b ON b.provider_campaign_id = c.provider_campaign_id\nGROUP BY c.provider_campaign_id, b.batches_fired, b.batches_total;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Three rules fall out of that query and they generalise beyond this platform.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Group by the campaign identifier, not the transaction identifier.<\/strong> Your own table needs a <code>provider_campaign_id<\/code> column, populated from <code>campaign\/read<\/code>, and every campaign-level number must aggregate across it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Never divide by the campaign total until every batch has fired.<\/strong> Report <code>delivered_pct_of_submitted<\/code> alongside <code>batches_fired \/ batches_total<\/code>, so a partial campaign reads as partial rather than as failing. This is the split-campaign form of the denominator bug described in <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/observability-for-messaging-pipelines\/\">observability for messaging pipelines<\/a>, and it has the same shape: the wrong denominator produces a number that is neither obviously right nor obviously wrong.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Count pending DLRs separately from failures.<\/strong> A message with no delivery report yet is not a failed message. Under splitting the awaiting-DLR bucket is large and legitimate for hours at a stretch, and folding it into failures is how a healthy campaign gets paged on.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"reporting-resolution\" class=\"wp-block-heading\">Which Report Resolves Which Level<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>You want<\/th><th>Endpoint<\/th><th>Key<\/th><th>Returns<\/th><\/tr><\/thead><tbody><tr><td>The batches of a campaign<\/td><td><code>SMSApi\/campaign\/read<\/code><\/td><td><code>campaignid<\/code><\/td><td>Batch manifest with <code>uuId<\/code>, <code>status<\/code>, <code>total<\/code>, timestamps<\/td><\/tr><tr><td>Everything about one batch<\/td><td><code>SMSApi\/reports\/status<\/code><\/td><td><code>uuid<\/code><\/td><td>Per-message rows for that one transaction<\/td><\/tr><tr><td>Per-message rows across a window<\/td><td><code>SMSApi\/reports\/status<\/code> with <code>method=getDlr<\/code><\/td><td>date range, optional <code>uuId<\/code> or <code>mobileNo<\/code><\/td><td>Paged DLR rows including <code>msgId<\/code>, <code>mobileNo<\/code>, <code>cause<\/code>, <code>cost<\/code><\/td><\/tr><tr><td>Totals for a period<\/td><td><code>SMSApi\/reports\/smsSummary<\/code><\/td><td>date range plus <code>groupby<\/code><\/td><td>Aggregate counters, no identifiers at all<\/td><\/tr><tr><td>A whole day, zipped<\/td><td><code>SMSApi\/report\/day<\/code><\/td><td>one <code>date<\/code><\/td><td>Bulk export, one day per call<\/td><\/tr><tr><td>Anything scheduled and not yet fired<\/td><td><code>SMSApi\/schedule\/read<\/code><\/td><td>none beyond auth<\/td><td>No date filter, no pagination, no status filter<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Two entries there need commentary.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>There is no campaign-level report endpoint.<\/strong> <code>reports\/status<\/code> resolves one <code>uuId<\/code> per call. To report on a six-batch campaign you make one <code>campaign\/read<\/code> call to get the six identifiers, then six <code>reports\/status<\/code> calls. Budget for that fan-out, cache the manifest, and do not put this loop on a page-load path.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>schedule\/read<\/code> takes no filter at all.<\/strong> No date range, no pagination, no status. It returns what it returns. That makes it usable as a small safety net for discovering pending work you have lost track of, and unusable as a primary index at any real volume. As covered previously, you cannot cancel or amend your own scheduled campaign unless you persisted its identifier at submission time, and splitting raises the cost of that oversight from one lost send to a whole campaign you cannot stop.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"schema-changes\" class=\"wp-block-heading\">Schema Changes You Need<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">If you already have the outbound message table from the <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/outbound-message-table-schema-design\/\">schema design article<\/a>, you need one new table and one new column. If you do not, read that article first; this section assumes it.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>-- Level one. One row per campaign the user asked for.\nCREATE TABLE campaign (\n    id                     BIGSERIAL PRIMARY KEY,\n    provider_campaign_id   TEXT UNIQUE,              -- campaignid, TEXT, may be NULL\n    internal_ref           TEXT NOT NULL UNIQUE,     -- your own key, always present\n    account_id             TEXT NOT NULL,\n    created_at             TIMESTAMPTZ NOT NULL DEFAULT now(),\n    manifest_fetched_at    TIMESTAMPTZ,\n    batches_expected       INT,                      -- from campaign\/read count\n    notes                  TEXT\n);\n\n-- Level two. One row per batch, discovered from campaign\/read.\nCREATE TABLE campaign_batch (\n    id                     BIGSERIAL PRIMARY KEY,\n    provider_campaign_id   TEXT NOT NULL REFERENCES campaign (provider_campaign_id),\n    uu_id                  TEXT NOT NULL UNIQUE,     -- transactionId, nineteen digits, TEXT\n    provider_status        TEXT,                     -- open enumeration, store raw\n    declared_total         INT,                      -- 'total', quoted string on the wire\n    created_ms             BIGINT,                   -- NULL when the sentinel '0' arrives\n    scheduled_ms           BIGINT,\n    updated_ms             BIGINT,\n    first_seen_at          TIMESTAMPTZ NOT NULL DEFAULT now(),\n    last_seen_at           TIMESTAMPTZ NOT NULL DEFAULT now()\n);\n\nCREATE INDEX ON campaign_batch (provider_campaign_id);\n\n-- Level three. Your existing per-recipient table gains a link upward.\nALTER TABLE outbound_message\n    ADD COLUMN uu_id TEXT REFERENCES campaign_batch (uu_id);\n\nCREATE INDEX ON outbound_message (uu_id);<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Four design decisions in there are deliberate and worth defending.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>internal_ref<\/code> is <code>NOT NULL<\/code> and <code>provider_campaign_id<\/code> is nullable.<\/strong> You always have your own key. You may never learn the provider&#8217;s, because nothing in a send response returns it. A schema that requires the provider identifier cannot represent the ordinary case.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>provider_status<\/code> stores the raw string.<\/strong> Map it in the query layer, not on ingest. When the platform introduces a status you have not seen, you want it in your database rather than coerced into <code>unknown<\/code> and lost.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Timestamps are millisecond epochs as <code>BIGINT<\/code>, with <code>NULL<\/code> for the <code>\"0\"<\/code> sentinel.<\/strong> Do not convert to <code>TIMESTAMPTZ<\/code> on ingest, because no timezone is documented for these values and an unlabelled conversion is an assumption you cannot verify. Store the raw epoch, convert at the presentation edge where the assumption is visible.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>declared_total<\/code> is what the platform said, not what you counted.<\/strong> Keeping the platform&#8217;s claim and your own count in separate columns is what makes the discrepancy visible. If they disagree, that is a finding, and a schema that overwrites one with the other destroys the evidence.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"reconciliation-playbook\" class=\"wp-block-heading\">The Reconciliation Playbook<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Run this as a scheduled job, not as a request handler.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Step one, on submit, persist your own intention.<\/strong> Write a <code>campaign<\/code> row with <code>internal_ref<\/code> before you call the API. Write the transaction identifier from the send response into <code>campaign_batch<\/code> as the first known batch, with <code>provider_campaign_id<\/code> still null. This gives you a record even if everything downstream fails.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Step two, obtain the campaign identifier.<\/strong> This is the hard part in a pure API integration, because no send response returns it. In practice you get it from the portal, from an operations runbook, or by correlating on submission time. Record how you obtained it. If you have no route to it at all, you have no campaign-level reconciliation and you should chunk client-side instead, where you generate the grouping key yourself.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Step three, fetch the manifest.<\/strong> Call <code>campaign\/read<\/code> and upsert one <code>campaign_batch<\/code> row per entry, keyed on <code>uu_id<\/code>. Set <code>batches_expected<\/code> on the campaign from <code>count<\/code>. Refresh the manifest on every reconciliation pass, because batch statuses change as later batches fire.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Step four, pull per-message rows for each batch.<\/strong> For each <code>uu_id<\/code> whose status is no longer pending, call <code>reports\/status<\/code> and upsert into <code>outbound_message<\/code> keyed on <code>msgId<\/code>. Upsert, not insert, because you will fetch overlapping windows and you must be idempotent. This is the same reasoning as in <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/message-idempotency-preventing-duplicate-sends\/\">message idempotency<\/a>, applied to reads rather than writes.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Step five, alert on the oldest stuck batch, not on the rate.<\/strong> The most useful signal for a split campaign is the age of the oldest batch still in <code>pending<\/code> past its <code>scheduled_ms<\/code>. That number goes up monotonically when something is wrong and returns to zero when it is not. A delivery rate, by contrast, moves for a dozen reasons and cannot distinguish a stalled scheduler from a bad recipient list.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Step six, close the campaign explicitly.<\/strong> A campaign is complete when every batch has a non-pending status and every message row has a terminal delivery status or has aged past your DLR window. Record the closure timestamp. Without an explicit close, &#8220;how did that campaign do&#8221; is a question with no defined answer, and every report you write will silently include campaigns still in flight.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"billing-under-split\" class=\"wp-block-heading\">Billing and Cost Under a Split<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Splitting does not change what you pay. The product documentation is explicit that no additional fee applies and that cost is based solely on the credits consumed, exactly as for a regular campaign.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">What splitting changes is <em>when<\/em> you pay, and that has real consequences.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Billing on this platform fires at submission, not at delivery. The published 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;. Under splitting, submission happens in instalments, so your wallet drains in instalments too.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Three practical implications:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>A campaign can run out of money halfway through.<\/strong> Batch one succeeds, your balance is consumed by other traffic in the intervening hours, and batch four fails on insufficient funds at three in the morning with nobody watching. Check <code>SMSApi\/account\/readstatus<\/code> before a scheduled campaign is due, not just before you submit it, and alert on balance falling below the cost of the remaining batches.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Your cost forecast must multiply correctly.<\/strong> The cost of a campaign is recipients times segments times rate, and the segment count depends on the rendered body, not the template. A single curly apostrophe flips a message from GSM-7 to Unicode and can triple its part count, which multiplies straight through every batch. Price the body with <code>SMSApi\/info\/msg<\/code> rather than counting characters yourself, and read <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/sms-length-calculation-credits-counted\/\">how SMS length and credits are counted<\/a> before you show any user a number.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Deleting a campaign does not refund the batches already submitted.<\/strong> Only pending batches are cancelled. Any user interface that offers a cancel button on a running split campaign should say precisely that, because the alternative is a support ticket asking where the money went. The rate slabs behind these calculations are set out in the <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/bulk-sms-pricing-in-india-what-actually-drives-cost\/\">bulk SMS pricing breakdown<\/a>.<\/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<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Requirement<\/th><th>Platform splitting<\/th><th>Client-side chunking<\/th><th>Neither, send in one go<\/th><\/tr><\/thead><tbody><tr><td>Spread inbound call volume over hours<\/td><td>Yes<\/td><td>Yes<\/td><td>No<\/td><\/tr><tr><td>Zero additional integration work<\/td><td>Yes<\/td><td>No<\/td><td>Yes<\/td><\/tr><tr><td>Survives your process crashing mid-campaign<\/td><td>Yes, the platform holds the schedule<\/td><td>No, unless you persist the queue<\/td><td>Not applicable<\/td><\/tr><tr><td>Cancel one specific pending batch<\/td><td>No<\/td><td>Yes<\/td><td>Not applicable<\/td><\/tr><tr><td>Change the body between batches<\/td><td>No<\/td><td>Yes<\/td><td>No<\/td><\/tr><tr><td>Different recipient segment per batch<\/td><td>No<\/td><td>Yes<\/td><td>No<\/td><\/tr><tr><td>One grouping key you control from the start<\/td><td>No, <code>campaignid<\/code> is provider-assigned<\/td><td>Yes, you mint it<\/td><td>Yes, one transaction<\/td><\/tr><tr><td>Per-batch cost visibility before sending<\/td><td>No<\/td><td>Yes, preview each chunk<\/td><td>Yes<\/td><\/tr><tr><td>Requires portal configuration<\/td><td>Yes<\/td><td>No<\/td><td>No<\/td><\/tr><tr><td>Reconciliation complexity<\/td><td>High, three levels<\/td><td>Medium, two levels plus your key<\/td><td>Low<\/td><\/tr><tr><td>Time-critical delivery, for example OTP<\/td><td>Never<\/td><td>Never<\/td><td>Yes<\/td><\/tr><tr><td>Recommended for a 50,000-recipient promotion where sales must handle callbacks<\/td><td>Yes<\/td><td>Only if you need per-batch control<\/td><td>No<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The honest summary: platform splitting is the right default when the requirement is &#8220;spread this out&#8221; and nothing more. Client-side chunking wins the moment you need per-batch control, per-batch content, or a grouping key you own. And splitting is never appropriate for time-critical traffic, because a one-time password that arrives three hours late is not a delayed one-time password, it is a failed authentication.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"build-order\" class=\"wp-block-heading\">Build Order<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Add the <code>campaign<\/code> and <code>campaign_batch<\/code> tables and the <code>uu_id<\/code> foreign key on <code>outbound_message<\/code>. Every identifier column is <code>TEXT<\/code>.<\/li>\n\n\n\n<li>Change your send-response parser into a union keyed on <code>sendMethod<\/code>. Delete every unconditional read of <code>response.mobile<\/code>.<\/li>\n\n\n\n<li>Add a boundary normaliser that converts the <code>\"0\"<\/code> sentinel and empty strings to null before anything else touches the payload.<\/li>\n\n\n\n<li>Persist your own <code>internal_ref<\/code> before the send call, not after it.<\/li>\n\n\n\n<li>Write the <code>campaign\/read<\/code> parser against the double-wrapped list shape, coercing quoted numerics once.<\/li>\n\n\n\n<li>Build the reconciliation job: refresh the manifest, then loop <code>reports\/status<\/code> per non-pending <code>uu_id<\/code>, upserting on <code>msgId<\/code>.<\/li>\n\n\n\n<li>Ingest the DLR feed, by webhook where possible, and make it authoritative for per-recipient rows. For group sends it is your only source.<\/li>\n\n\n\n<li>Replace every delivery-rate query with the submitted-denominator form, and surface <code>batches_fired \/ batches_total<\/code> next to it.<\/li>\n\n\n\n<li>Add the oldest-pending-batch-age metric and alert on it. Do not alert on delivery rate for split campaigns.<\/li>\n\n\n\n<li>Add a pre-fire balance check that runs before each scheduled batch is due, comparing balance against the cost of remaining batches.<\/li>\n\n\n\n<li>Write a contract test that submits with <code>testMessage=true<\/code>, reads back through <code>campaign\/read<\/code> and <code>reports\/status<\/code>, and asserts your parsers still match the live shapes. Run it nightly. The reasoning is in <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/testing-code-that-sends-messages\/\">testing code that sends messages<\/a>.<\/li>\n\n\n\n<li>Document, in your own runbook, how an operator obtains a <code>campaignid<\/code> for a campaign your service submitted. If that procedure does not exist, campaign-level reconciliation does not exist either.<\/li>\n<\/ol>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"ten-mistakes\" class=\"wp-block-heading\">Ten Mistakes<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Treating the transaction identifier as the campaign identifier.<\/strong> It names one batch. Under splitting it names a fraction of the work.<\/li>\n\n\n\n<li><strong>Storing identifiers as integers.<\/strong> Nineteen digits, and seventeen for campaign identifiers, both beyond exact representation as a double.<\/li>\n\n\n\n<li><strong>Dividing delivered by the campaign total before every batch has fired.<\/strong> Produces an alarming number that is arithmetically meaningless.<\/li>\n\n\n\n<li><strong>Reading <code>response.mobile<\/code> after a group send.<\/strong> The field does not exist in that response shape.<\/li>\n\n\n\n<li><strong>Parsing <code>msg<\/code> to determine what happened.<\/strong> The campaign endpoints return schedule wording. Branch on <code>api<\/code>, <code>action<\/code> and <code>code<\/code>.<\/li>\n\n\n\n<li><strong>Converting <code>lastupdatedTimestamp<\/code> of <code>\"0\"<\/code> to a date.<\/strong> It means never, and it renders as 1970.<\/li>\n\n\n\n<li><strong>Sharing one schedule-time formatter across SMS send, campaign update and WhatsApp send.<\/strong> Three spellings, two formats.<\/li>\n\n\n\n<li><strong>Assuming <code>campaign\/update<\/code> can change the message body.<\/strong> The prose implies it. The parameter table refutes it.<\/li>\n\n\n\n<li><strong>Offering a cancel button that implies a refund.<\/strong> Delete stops pending batches only, and submitted credits are non-refundable.<\/li>\n\n\n\n<li><strong>Using splitting for one-time passwords or any time-critical alert.<\/strong> The whole mechanism is a deliberate delay.<\/li>\n<\/ol>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"checklist\" class=\"wp-block-heading\">Pre-Launch Checklist<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Schema and types<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>provider_campaign_id<\/code>, <code>uu_id<\/code> and <code>msg_id<\/code> are all <code>TEXT<\/code><\/li>\n\n\n\n<li><code>internal_ref<\/code> is <code>NOT NULL UNIQUE<\/code> and written before the send call<\/li>\n\n\n\n<li><code>provider_campaign_id<\/code> is nullable and the code path where it is null is tested<\/li>\n\n\n\n<li><code>declared_total<\/code> and your own counted total are separate columns<\/li>\n\n\n\n<li>Millisecond epochs stored as <code>BIGINT<\/code>, not converted on ingest<\/li>\n\n\n\n<li>Index on <code>campaign_batch (provider_campaign_id)<\/code> and <code>outbound_message (uu_id)<\/code><\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Parsing<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Send-response parser is a union keyed on <code>sendMethod<\/code><\/li>\n\n\n\n<li>Group-send path never reads <code>mobile<\/code> or <code>invalidMobile<\/code><\/li>\n\n\n\n<li><code>campaignList<\/code> double wrapping handled at <code>[i].campaign<\/code><\/li>\n\n\n\n<li>All quoted numerics coerced once, at the boundary<\/li>\n\n\n\n<li><code>\"0\"<\/code> and <code>\"\"<\/code> normalised to null before domain code sees them<\/li>\n\n\n\n<li><code>status<\/code> treated as an open enumeration, raw value retained<\/li>\n\n\n\n<li>No code path branches on the text of <code>msg<\/code><\/li>\n\n\n\n<li>JSON parsing verified not to coerce long identifiers to doubles<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Reconciliation<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Manifest refreshed on every pass, not cached indefinitely<\/li>\n\n\n\n<li><code>reports\/status<\/code> loop is per <code>uu_id<\/code> and upserts on <code>msgId<\/code><\/li>\n\n\n\n<li>Reconciliation runs on a schedule, not in a request handler<\/li>\n\n\n\n<li>Overlapping windows are safe because every write is an upsert<\/li>\n\n\n\n<li>Campaign closure is explicit and timestamped<\/li>\n\n\n\n<li>DLR feed treated as authoritative for per-recipient rows<\/li>\n\n\n\n<li>Webhook endpoint is idempotent and returns quickly<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Reporting and alerting<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Delivery rate uses submitted as the denominator<\/li>\n\n\n\n<li><code>batches_fired \/ batches_total<\/code> displayed alongside every campaign rate<\/li>\n\n\n\n<li>Awaiting-DLR counted separately from failed<\/li>\n\n\n\n<li>Oldest-pending-batch age is a monitored metric<\/li>\n\n\n\n<li>No alert fires on delivery rate for a campaign with pending batches<\/li>\n\n\n\n<li>Dashboards group by <code>provider_campaign_id<\/code>, never by <code>uu_id<\/code> alone<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Operations<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Balance checked before each scheduled batch is due, not only at submit<\/li>\n\n\n\n<li>Alert when balance falls below the cost of remaining batches<\/li>\n\n\n\n<li>Runbook documents how to obtain a <code>campaignid<\/code><\/li>\n\n\n\n<li>Runbook states that delete is campaign-wide and cannot target one batch<\/li>\n\n\n\n<li>User-facing cancel copy states that sent batches are not refunded<\/li>\n\n\n\n<li>Splitting explicitly prohibited for one-time passwords and time-critical alerts<\/li>\n\n\n\n<li>Nightly contract test asserts parsers against live response shapes<\/li>\n\n\n\n<li>Someone owns the portal configuration and changes to it are recorded<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"not-claimed\" class=\"wp-block-heading\">What This Article Deliberately Does Not Claim<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Nine things about split campaigns that this article will not assert, because they are not documented on SMSGatewayCenter&#8217;s own pages and guessing would be worse than a gap.<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>The permitted batch size range.<\/strong> Two live SMSGatewayCenter pages describe different minimums. Read the value from the portal at the moment you configure the campaign and do not hard-code it.<\/li>\n\n\n\n<li><strong>The available interval options.<\/strong> Same reason. The dropdown is the authority.<\/li>\n\n\n\n<li><strong>Whether a send response ever contains <code>campaignid<\/code>.<\/strong> No documented sample includes it. This article assumes it does not and describes the consequences of that assumption rather than asserting the platform&#8217;s internals.<\/li>\n\n\n\n<li><strong>Whether <code>campaign\/update<\/code> reschedules the first batch only, or shifts every remaining batch by the same offset.<\/strong> The parameter is documented as &#8220;your new time to update the campaign time&#8221; and no sample distinguishes the two behaviours. Test it on your own account before relying on either.<\/li>\n\n\n\n<li><strong>What happens to a split campaign whose account runs out of credit mid-run.<\/strong> Whether remaining batches fail, pause or are cancelled is not documented.<\/li>\n\n\n\n<li><strong>Whether group membership is resolved at composition time or at each batch&#8217;s fire time.<\/strong> This materially changes who receives batch six of a group send and it is not stated anywhere.<\/li>\n\n\n\n<li><strong>The retention window for campaign records.<\/strong> How long <code>campaign\/read<\/code> will still resolve a <code>campaignid<\/code> is not published.<\/li>\n\n\n\n<li><strong>Whether splitting interacts with <code>duplicatecheck<\/code>.<\/strong> Whether deduplication runs across the whole recipient list or within each batch is undocumented, and the two behaviours produce different recipient counts.<\/li>\n\n\n\n<li><strong>Any throughput, latency or per-batch timing precision figure.<\/strong> None is published, and inventing one would be exactly the kind of number that gets quoted back in a contract negotiation.<\/li>\n<\/ol>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"faqs\" class=\"wp-block-heading\">FAQs<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>What is a split SMS campaign?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A split campaign takes one large recipient list and submits it in several smaller batches at fixed intervals rather than all at once. It is configured in the SMSGatewayCenter portal by enabling the split option, setting a batch limit and choosing an interval. The first batch fires immediately or at the scheduled time, and each subsequent batch fires after the interval elapses.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Can I create a split campaign through the API?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">No. There is no documented parameter on <code>SMSApi\/send<\/code> that enables splitting or sets a batch size or interval. Splitting is configured in the portal. The API can read, reschedule and delete a split campaign, but it cannot create one.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>What is the difference between campaignid and transactionId?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>campaignid<\/code> identifies the whole campaign and is the only key accepted by the campaign read, update and delete endpoints. <code>transactionId<\/code> identifies one batch, one submission. Under splitting, one <code>campaignid<\/code> corresponds to several transaction identifiers. The same value appears as <code>uuId<\/code> in report responses.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Why does my delivery rate look terrible right after I send?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Almost certainly because you are dividing delivered messages by the campaign total while only the first batch has actually been submitted. Divide by messages submitted so far and display the fired-batch count alongside, and the number becomes meaningful.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>How do I find all the batches in a campaign?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Call <code>SMSApi\/campaign\/read<\/code> with the <code>campaignid<\/code>. It returns a <code>campaignList<\/code> array with one entry per batch, each carrying a <code>uuId<\/code>, a status, a total and timestamps. That is the only endpoint that enumerates batches.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Can I cancel just one batch?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Not through the API. <code>SMSApi\/campaign\/delete<\/code> is keyed on <code>campaignid<\/code> and addresses every remaining batch. If per-batch cancellation is a genuine requirement, chunk client-side instead so that each chunk is an independently addressable scheduled send.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Can I change the message text of a campaign that has not finished sending?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Not through the API. <code>SMSApi\/campaign\/update<\/code> accepts only <code>campaignid<\/code> and <code>scheduletime<\/code>. The introductory prose on that documentation page mentions content, but the parameter table does not include a content field.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Does splitting cost extra?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">No. The product documentation states that no additional fee applies and that cost is based solely on the SMS credits consumed. What changes is the timing: because billing fires at submission, your wallet is debited in instalments as each batch goes out.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Why does my group send response not list the recipients?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Because the group-send response shape does not include <code>mobile<\/code> or <code>invalidMobile<\/code>. It returns the group identifier and a transaction identifier only. For a group send, the delivery report feed is the only place recipients appear, which makes DLR ingestion mandatory rather than optional.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Should I use splitting for OTP messages?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">No. Splitting is a deliberate delay mechanism and one-time passwords are the most latency-sensitive traffic on the platform. The OTP endpoints are also a separate path with their own constraints, including a transaction identifier that is documented as the literal string zero and therefore carries no correlation value.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>How do I know whether splitting is even switched on?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">There is no flag in any send response. You infer it by holding a <code>campaignid<\/code>, calling <code>campaign\/read<\/code> and observing more than one entry. The right response to this is not to detect splitting but to write code that is correct for N batches, with N equal to one in the ordinary case.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Is splitting the same as rate limiting?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">No, and conflating them causes real design errors. Rate limiting shapes your request rate to stay inside what the gateway will accept, over seconds and minutes. Splitting spreads delivery over hours to manage the human response to a campaign. They solve unrelated problems and you may well need both.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>What should I alert on for a split campaign?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The age of the oldest batch still pending past its scheduled time. That metric rises monotonically when the scheduler is stuck and sits at zero when it is not. Delivery rate is a poor alerting signal here because it legitimately looks bad for hours.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Do I need the campaign endpoints if I never split anything?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Probably not today, but the cost of building for N batches instead of one is a table and a join, and the cost of retrofitting it after someone enables splitting in the portal without telling engineering is a week of confusing dashboards. Build it once.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Ready to Build Split Campaigns?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Building a bulk messaging integration that needs to handle split campaigns, delivery reconciliation and per-batch reporting correctly? <a href=\"https:\/\/www.smsgatewaycenter.com\/contact\/\">Talk to the SMSGatewayCenter team<\/a> about your volumes and campaign patterns, or read the <a href=\"https:\/\/www.smsgatewaycenter.com\/developer-api\/\">full API documentation<\/a> to see the campaign and report endpoints in detail.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n","protected":false},"excerpt":{"rendered":"<p>A split SMS campaign turns one send into several transactions, each with its own identifier. Here is what the campaign, transaction and message levels actually mean, which SMSGatewayCenter endpoint resolves each one, and how to aggregate and reconcile a campaign without double counting or reporting a false delivery rate.<\/p>\n","protected":false},"author":118,"featured_media":2945,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[268],"tags":[86,2218,2220,2016,2106,2221,481,27,1212,2219],"class_list":["post-2944","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-tutorials","tag-bulk-sms","tag-campaign-api","tag-campaign-id","tag-delivery-reports","tag-messaging-infrastructure","tag-reconciliation","tag-sms-api","tag-sms-gateway-center","tag-split-sms-campaign","tag-transaction-id"],"_links":{"self":[{"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/posts\/2944","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=2944"}],"version-history":[{"count":0,"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/posts\/2944\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/media\/2945"}],"wp:attachment":[{"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/media?parent=2944"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/categories?post=2944"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/tags?post=2944"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}