{"id":3059,"date":"2026-10-02T11:15:52","date_gmt":"2026-10-02T05:45:52","guid":{"rendered":"https:\/\/www.smsgatewaycenter.com\/blog\/?p=3059"},"modified":"2026-10-02T11:19:34","modified_gmt":"2026-10-02T05:49:34","slug":"scheduling-messages-sms-whatsapp-rcs-telegram","status":"publish","type":"post","link":"https:\/\/www.smsgatewaycenter.com\/blog\/scheduling-messages-sms-whatsapp-rcs-telegram\/","title":{"rendered":"How to Schedule Messages on SMS, WhatsApp, RCS and Telegram (and Why You Still Need Your Own Scheduler)"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">You can schedule an SMS through the API and change your mind later. WhatsApp lets you book but never undo. RCS and Telegram don&#8217;t let you schedule at all. Here&#8217;s how each one really works, how to find out which time zone your timestamps land in, and how to build one scheduler that behaves the same on all four.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/10\/scheduling-messages-sms-whatsapp-rcs-telegram.webp\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"584\" src=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/10\/scheduling-messages-sms-whatsapp-rcs-telegram-1024x584.webp\" alt=\"Abstract diagram of four message lanes with clock rings, two lanes fed by one shared scheduler on the left\" class=\"wp-image-3060\" srcset=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/10\/scheduling-messages-sms-whatsapp-rcs-telegram-1024x584.webp 1024w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/10\/scheduling-messages-sms-whatsapp-rcs-telegram-300x171.webp 300w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/10\/scheduling-messages-sms-whatsapp-rcs-telegram-768x438.webp 768w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/10\/scheduling-messages-sms-whatsapp-rcs-telegram.webp 1200w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><figcaption class=\"wp-element-caption\">Two channels can hold a schedule for you. All four need a clock you own.<\/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=\"#four-contracts\">What Each Channel Actually Lets You Do<\/a><\/li>\n\n\n\n<li><a href=\"#where-schedule-lives\">Where Should the Schedule Live?<\/a><\/li>\n\n\n\n<li><a href=\"#sms-lifecycle\">Scheduling an SMS, Start to Finish<\/a><\/li>\n\n\n\n<li><a href=\"#identifier-spellings\">One ID, Three Names<\/a><\/li>\n\n\n\n<li><a href=\"#time-zone\">The Time Zone Problem<\/a><\/li>\n\n\n\n<li><a href=\"#timezone-probe\">Finding the Time Zone With One Test Booking<\/a><\/li>\n\n\n\n<li><a href=\"#whatsapp-scheduling\">WhatsApp: You Can Book It, but You Can&#8217;t Take It Back<\/a><\/li>\n\n\n\n<li><a href=\"#rcs-telegram\">RCS and Telegram: Bring Your Own Clock<\/a><\/li>\n\n\n\n<li><a href=\"#promotional-hours\">Promotional Hours and Store and Forward<\/a><\/li>\n\n\n\n<li><a href=\"#fire-time-risks\">What Can Go Wrong While a Message Waits<\/a><\/li>\n\n\n\n<li><a href=\"#cancellation-race\">The Cancel That Arrives Too Late<\/a><\/li>\n\n\n\n<li><a href=\"#hybrid-scheduler\">Putting It Together: A Hybrid Scheduler<\/a><\/li>\n\n\n\n<li><a href=\"#schedule-table\">The Schedule Table<\/a><\/li>\n\n\n\n<li><a href=\"#reconciliation\">Checking Your Schedule Against the Platform<\/a><\/li>\n\n\n\n<li><a href=\"#code-samples\">Code: Four Samples, Four Gotchas<\/a><\/li>\n\n\n\n<li><a href=\"#decision-matrix\">Which Path Should This Message Take?<\/a><\/li>\n\n\n\n<li><a href=\"#testing-scheduler\">Testing a Scheduler Without Waiting for the Clock<\/a><\/li>\n\n\n\n<li><a href=\"#checklist\">Before You Go Live<\/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\n\n\n<li><\/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\"><a href=\"https:\/\/www.smsgatewaycenter.com\/transactional-sms\/\">SMS<\/a> is the only channel where the API lets you schedule a message and then change your mind. You add <code>scheduleTime<\/code> to <code>SMSApi\/send<\/code>, and later you can list it, move it or cancel it with the <code>SMSApi\/schedule\/read<\/code>, <code>update<\/code> and <code>delete<\/code> endpoints.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">WhatsApp will take a <code>scheduletime<\/code> on <code>WAApi\/send<\/code> (note the lower-case t, and no seconds), but that&#8217;s a one-shot deal. There&#8217;s no way to look at it, move it or cancel it afterwards. RCS and Telegram don&#8217;t accept a schedule time through the API at all.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">So if your app has a &#8220;send later&#8221; button, you&#8217;re going to end up running your own scheduler anyway. The sensible setup is to keep every scheduled message in your own database, send RCS and Telegram yourself when the time comes, and only hand SMS over to the platform shortly before it&#8217;s due, once you&#8217;ve confirmed which time zone it reads your timestamps in.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Two other things will bite you if you don&#8217;t plan for them. The API never tells you what time zone it uses, but its read endpoint returns plain epoch timestamps, so one test booking is enough to find out. And if you send promotional SMS outside the allowed hours, the Store and Forward OWH setting can quietly hold it until the next morning.<\/p>\n\n\n\n<h2 id=\"tldr\" class=\"wp-block-heading\">TL;DR<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Question<\/th><th>Answer<\/th><\/tr><\/thead><tbody><tr><td>Which channels can I schedule through the API?<\/td><td>SMS (<code>scheduleTime<\/code> on <code>SMSApi\/send<\/code>) and WhatsApp (<code>scheduletime<\/code> on <code>WAApi\/send<\/code>). RCS and Telegram can&#8217;t be scheduled through the API.<\/td><\/tr><tr><td>Which ones can I change or cancel afterwards?<\/td><td>Only SMS, with <code>SMSApi\/schedule\/read<\/code>, <code>SMSApi\/schedule\/update<\/code> and <code>SMSApi\/schedule\/delete<\/code>. Split campaigns move with <code>SMSApi\/campaign\/update<\/code>.<\/td><\/tr><tr><td>What format do they want?<\/td><td>SMS: <code>YYYY-MM-DD HH:MM:SS<\/code>. WhatsApp: <code>YYYY-MM-DD HH:MM<\/code>. Neither takes a time zone or offset.<\/td><\/tr><tr><td>What do I get back when I read a booking?<\/td><td><code>uuId<\/code>, <code>status<\/code>, <code>total<\/code>, <code>timestamp<\/code>, <code>scheduledTimestamp<\/code>, <code>lastupdatedTimestamp<\/code>, all as strings, times in epoch milliseconds.<\/td><\/tr><tr><td>What&#8217;s the ID called?<\/td><td>Depends where you look: <code>transactionId<\/code> when you send, <code>uuid<\/code> when you update or delete, <code>uuId<\/code> when you read. It&#8217;s the same value. Keep it as text.<\/td><\/tr><tr><td>What time zone does it use?<\/td><td>Don&#8217;t assume. Book one test send, read it back, and compare.<\/td><\/tr><tr><td>What about promotional hours?<\/td><td>Promotional and service-explicit SMS sent outside the posting window can be held until it reopens. Transactional SMS always goes straight out.<\/td><\/tr><tr><td>Where should my schedule live?<\/td><td>In your own database. Treat the platform&#8217;s scheduler as a last-minute handoff for SMS.<\/td><\/tr><tr><td>What should I check right before sending?<\/td><td>Balance, template status, sender ID, whether the person opted out, and whether anyone cancelled.<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"four-contracts\" class=\"wp-block-heading\">What Each Channel Actually Lets You Do<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">When you schedule anything, you need to know six things: can I book it, what format does it want, can I see it afterwards, can I move it, can I cancel it, and what ID do I use to do all that? Here&#8217;s how the four channels answer.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/10\/diagram-scheduling-capability-matrix.svg\"><img decoding=\"async\" src=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/10\/diagram-scheduling-capability-matrix.svg\" alt=\"Table comparing six scheduling capabilities across SMS quick and group sends, SMS split campaigns, WhatsApp, RCS and Telegram\" class=\"wp-image-3061\"\/><\/a><figcaption class=\"wp-element-caption\">Only the SMS paths have a full book, read, move and cancel lifecycle. WhatsApp is book-only. RCS and Telegram need your own clock.<\/figcaption><\/figure>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Capability<\/th><th>SMS (quick, group, file)<\/th><th>SMS split campaign<\/th><th>WhatsApp<\/th><th>RCS<\/th><th>Telegram<\/th><\/tr><\/thead><tbody><tr><td>Book a future send<\/td><td><code>scheduleTime<\/code> on <code>SMSApi\/send<\/code><\/td><td>Created in the portal<\/td><td><code>scheduletime<\/code> on <code>WAApi\/send<\/code><\/td><td>No API parameter<\/td><td>No API parameter<\/td><\/tr><tr><td>Format<\/td><td><code>YYYY-MM-DD HH:MM:SS<\/code><\/td><td>n\/a<\/td><td><code>YYYY-MM-DD HH:MM<\/code><\/td><td>n\/a<\/td><td>n\/a<\/td><\/tr><tr><td>Read back<\/td><td><code>SMSApi\/schedule\/read<\/code><\/td><td><code>SMSApi\/campaign\/read<\/code><\/td><td>No endpoint<\/td><td>Portal list only<\/td><td>n\/a<\/td><\/tr><tr><td>Move<\/td><td><code>SMSApi\/schedule\/update<\/code> (<code>uuid<\/code>, <code>scheduletime<\/code>)<\/td><td><code>SMSApi\/campaign\/update<\/code> (<code>campaignid<\/code>, <code>scheduletime<\/code>)<\/td><td>No endpoint<\/td><td>Portal only<\/td><td>n\/a<\/td><\/tr><tr><td>Cancel<\/td><td><code>SMSApi\/schedule\/delete<\/code> (<code>uuid<\/code>)<\/td><td><code>SMSApi\/campaign\/delete<\/code><\/td><td>No endpoint<\/td><td>Portal only<\/td><td>n\/a<\/td><\/tr><tr><td>Identifier<\/td><td><code>transactionId<\/code> -&gt; <code>uuid<\/code> -&gt; <code>uuId<\/code><\/td><td><code>campaignid<\/code> plus per-batch <code>uuId<\/code><\/td><td><code>messageId<\/code><\/td><td>n\/a<\/td><td>n\/a<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">SMS is the easy one. You book it, you get an ID back, and with that ID you can look it up, move it or cancel it whenever you like. That&#8217;s what makes it safe to let the platform hold an SMS for you.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">WhatsApp is the trap. Booking works fine, but the WhatsApp section of the API reference has sending, media, templates, reports, inbox and analytics, and nothing for schedules. Once you&#8217;ve sent <code>scheduletime<\/code>, that message is going out at that time and there&#8217;s nothing you can do about it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/www.smsgatewaycenter.com\/rcs-messaging\/\">RCS<\/a> is a bit odd. You can schedule RCS campaigns in the portal, and the <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/kb\/how-do-i-schedule-an-rcs-campaign-and-can-i-change-or-cancel-it\/\">RCS scheduling help article<\/a> shows how to change or cancel them there. But <code>RCSApi\/send<\/code> only takes <code>sendMethod<\/code>, <code>msgType<\/code>, <code>format<\/code>, <code>botId<\/code>, <code>msg<\/code>, <code>mobile<\/code> and an optional <code>identifier<\/code>. No schedule time. <a href=\"https:\/\/www.smsgatewaycenter.com\/telegram-bulk-messaging\/\">Telegram<\/a>&#8216;s <code>rest\/tg\/v1\/send<\/code> is the same story. If you&#8217;re integrating through the API, both of them are send-now only.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Which means if you offer scheduling on more than one channel, you&#8217;re writing a scheduler. There&#8217;s no way around that. What you get to decide is how much of the actual sending you let the platform do.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"where-schedule-lives\" class=\"wp-block-heading\">Where Should the Schedule Live?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">You&#8217;ve got three options, and they each fail in their own way.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">You can let the platform hold it. You submit the message now with a future time, and it goes out on schedule even if your whole stack is down. That&#8217;s genuinely useful: a bad deploy at 08:59 won&#8217;t make you miss a 09:00 reminder. The downside is that the message is now frozen. You can still move or cancel an SMS, but you can&#8217;t edit the text, and on WhatsApp you can&#8217;t do anything at all.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">You can hold it yourself. Store it in your database and have a worker send it (without any schedule parameter) when it&#8217;s due. Now you can change the text, re-check consent and cancel right up to the last second, and the same code works for every channel. The cost is that if your worker is down at 09:00, the message is late.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Or you can do both, which is what we&#8217;ll build here. Your database is always the master copy. A little before an SMS is due, a worker hands it to the platform with <code>scheduleTime<\/code>, saves the ID, and reads the booking back to make sure it&#8217;s right. If something changes after that, you call update or delete. RCS, Telegram, and any WhatsApp message someone might want to cancel never get handed off; your own worker sends them when they&#8217;re due.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Here&#8217;s how the three stack up:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th><\/th><th>Platform holds it<\/th><th>You hold it<\/th><th>Both<\/th><\/tr><\/thead><tbody><tr><td>Works on all four channels<\/td><td>No<\/td><td>Yes<\/td><td>Yes<\/td><\/tr><tr><td>Still sends if your workers are down<\/td><td>Yes, on SMS and WhatsApp<\/td><td>No<\/td><td>Yes, once handed off<\/td><\/tr><tr><td>Can edit the text after booking<\/td><td>No (cancel and rebook on SMS)<\/td><td>Yes<\/td><td>Yes, until handoff<\/td><\/tr><tr><td>Can cancel right up to send time<\/td><td>SMS only<\/td><td>Yes<\/td><td>Yes, with delete after handoff on SMS<\/td><\/tr><tr><td>Consent re-checked before sending<\/td><td>No<\/td><td>Yes<\/td><td>Yes, up to handoff<\/td><\/tr><tr><td>One place to see everything scheduled<\/td><td>No, read is SMS-only and unfiltered<\/td><td>Yes<\/td><td>Yes<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"sms-lifecycle\" class=\"wp-block-heading\">Scheduling an SMS, Start to Finish<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Everything below hits <code>https:\/\/unify.smsgateway.center\/<\/code>. Send the <code>apikey<\/code> header with <code>userid<\/code> on each call; the header works on every endpoint.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">To book, POST to <code>SMSApi\/send<\/code> exactly as you normally would (<code>sendMethod<\/code>, <code>mobile<\/code> or <code>group<\/code>, <code>msg<\/code>, <code>senderid<\/code>, <code>msgType<\/code>, <code>output<\/code>) and add <code>scheduleTime<\/code>. Both the quick and group send pages list it as &#8220;Date format YYYY-MM-DD HH:MM:SS&#8221;. The response looks just like a normal send:<\/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\">Save that <code>transactionId<\/code> straight away, as text. Without it you can&#8217;t move or cancel the message later. If your <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/outbound-message-table-schema-design\/\">outbound message table<\/a> already keeps it for delivery reports, good, just make sure scheduled sends write it too.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">To see what&#8217;s pending, call <code>SMSApi\/schedule\/read<\/code> (POST or GET) with <code>output=json<\/code>. You can&#8217;t filter it or page through it. It just returns everything:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"response\": {\n    \"api\": \"schedule\",\n    \"action\": \"read\",\n    \"status\": \"success\",\n    \"msg\": \"success\",\n    \"code\": \"200\",\n    \"count\": 1,\n    \"scheduleList\": &#91;\n      {\n        \"schedule\": {\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\">A couple of things to notice. Each row is wrapped in a <code>schedule<\/code> object. <code>count<\/code> is a real number, but <code>total<\/code> (how many recipients) is a string. The three timestamps are epoch milliseconds, also as strings: <code>timestamp<\/code> is when you booked it, <code>scheduledTimestamp<\/code> is when it&#8217;ll go out, and <code>lastupdatedTimestamp<\/code> of <code>\"0\"<\/code> just means nobody has moved it yet. It&#8217;s not a date in 1970.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">To move a booking, POST to <code>SMSApi\/schedule\/update<\/code> with <code>uuid<\/code>, the new <code>scheduletime<\/code> (same format) and <code>output<\/code>. The page says it&#8217;s POST only. You&#8217;ll get back <code>\"msg\":\"Schedule time updated successfully.\"<\/code> with <code>\"status\":\"success\"<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">To cancel, POST to <code>SMSApi\/schedule\/delete<\/code> with <code>uuid<\/code> and <code>output<\/code>. You&#8217;ll get <code>\"msg\":\"Schedule deleted successfully\"<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Neither of those tells you what things look like afterwards, so read the list again and check. It&#8217;s one extra call, and it means you actually know the booking moved or disappeared instead of just trusting a success message.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/www.smsgatewaycenter.com\/split-sms-campaigns\/\">Split campaigns<\/a>, where one booking goes out in several batches, work the same way but use <code>campaignid<\/code>: <code>SMSApi\/campaign\/read<\/code> (with <code>campaignid<\/code>, optional <code>uuid<\/code>, <code>fromdate<\/code>, <code>todate<\/code>), <code>SMSApi\/campaign\/update<\/code> (<code>campaignid<\/code>, <code>scheduletime<\/code>) and <code>SMSApi\/campaign\/delete<\/code>. You set those campaigns up in the portal, not through the API. The <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/campaign-splitting-one-send-several-transactions\/\">campaign splitting guide<\/a> goes deeper, and everything below about time zones and promotional hours applies to them as well.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"identifier-spellings\" class=\"wp-block-heading\">One ID, Three Names<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This one catches people. The ID you use to manage a booking has a different name at every step:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Where<\/th><th>Name<\/th><th>JSON type<\/th><th>Example<\/th><\/tr><\/thead><tbody><tr><td><code>SMSApi\/send<\/code> response<\/td><td><code>transactionId<\/code><\/td><td>quoted string<\/td><td><code>\"6305583318236810379\"<\/code><\/td><\/tr><tr><td><code>SMSApi\/schedule\/update<\/code> and <code>\/delete<\/code> request<\/td><td><code>uuid<\/code><\/td><td>form field<\/td><td><code>uuid=83147103413249843<\/code><\/td><\/tr><tr><td><code>SMSApi\/schedule\/read<\/code> response<\/td><td><code>uuId<\/code><\/td><td>quoted string<\/td><td><code>\"5718742519829975000\"<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">It&#8217;s the same value every time. The easiest way to stay sane is to give it one name in your own code (something like <code>provider_txn_id<\/code>) and only deal with the three API spellings inside the bits of code that talk to the API.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The other thing: never let it become a number. These IDs can be nineteen digits. A double can only store whole numbers exactly up to 2^53, which is sixteen digits, so anything longer gets rounded. Push <code>\"6305583318236810379\"<\/code> through a JavaScript <code>Number<\/code> or a Python <code>float<\/code> and you get 6305583318236810240. Try to cancel with that and you&#8217;ll be cancelling something that doesn&#8217;t exist, while the real message goes out on time. The API sends these as quoted strings, so this only happens if your own code converts them: a stray <code>parseInt<\/code>, a <code>BIGINT<\/code> column in a language that maps it to a double, or a round trip through Excel. Store them as text, compare them as text, and you&#8217;re fine.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"time-zone\" class=\"wp-block-heading\">The Time Zone Problem<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">When you send <code>2026-10-05 09:30:00<\/code>, you&#8217;re sending a clock reading, not a moment in time. 09:30 where? The API doesn&#8217;t take an offset, so the server decides.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/kb\/how-do-i-schedule-an-rcs-campaign-and-can-i-change-or-cancel-it\/\">RCS scheduling help article<\/a> mentions that the portal shows times &#8220;in your account&#8217;s time zone&#8221; and that the update screen shows &#8220;the current server time&#8221; for reference. So there are at least two clocks around, and nothing tells you which one the API uses for your timestamp.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If your account is in India, it&#8217;s almost certainly IST (UTC+05:30) and you&#8217;d probably be fine guessing. But don&#8217;t bake that guess into your code, because:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>If you&#8217;re wrong, everything is off by five and a half hours. Your 09:00 reminder lands at 14:30, or, if you&#8217;re wrong the other way, your promotional blast goes out at 03:30 and runs straight into restricted hours.<\/li>\n\n\n\n<li>Your servers are probably on UTC. Somebody writes <code>datetime.now() + timedelta(hours=2)<\/code>, gets a UTC clock time, and it works fine on their laptop in Bengaluru and breaks in production.<\/li>\n\n\n\n<li>Not every account is in India, and an account&#8217;s zone setting won&#8217;t always match the default.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Luckily you don&#8217;t have to guess. The read endpoint gives you back epoch milliseconds, which don&#8217;t have a time zone. Compare that with what you sent and you know exactly how your timestamp was read.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"timezone-probe\" class=\"wp-block-heading\">Finding the Time Zone With One Test Booking<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">You only need to do this once per account, and again if someone changes the account&#8217;s settings.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Step one: book a test send.<\/strong> Pick a number you own, a transactional template, and a time at least a day away at an easy-to-spot minute, say <code>2026-10-09 11:17:00<\/code>. Note exactly what you sent and the <code>transactionId<\/code> you got back.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Step two: read it back.<\/strong> Call <code>SMSApi\/schedule\/read<\/code>, find the row whose <code>uuId<\/code> matches your <code>transactionId<\/code> (compare them as strings), and take its <code>scheduledTimestamp<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Step three: see which zone matches.<\/strong> Convert your timestamp to epoch milliseconds as if it were UTC, then as if it were Asia\/Kolkata, and any other zone you suspect. One of them will match what came back.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Step four: save it and clean up.<\/strong> Store the zone against the account, cancel the test with <code>SMSApi\/schedule\/delete<\/code>, and read once more to make sure it&#8217;s gone.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>from datetime import datetime\nfrom zoneinfo import ZoneInfo\n\ndef epoch_ms(wall: str, tz: str) -&gt; int:\n    dt = datetime.strptime(wall, \"%Y-%m-%d %H:%M:%S\").replace(tzinfo=ZoneInfo(tz))\n    return int(dt.timestamp() * 1000)\n\ndef infer_zone(sent_wall: str, returned_ms: str, candidates: list&#91;str]) -&gt; str | None:\n    target = int(returned_ms)            # quoted string on the wire; parse locally only\n    for tz in candidates:\n        if epoch_ms(sent_wall, tz) == target:\n            return tz\n    return None                          # no match: stop and investigate, do not default\n\nzone = infer_zone(\"2026-10-09 11:17:00\", \"1791524820000\",\n                  &#91;\"UTC\", \"Asia\/Kolkata\"])<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If nothing matches, don&#8217;t just fall back to IST. Either you grabbed the wrong row or the platform is using a zone you didn&#8217;t think of, and you want a human to look at that before real customers get scheduled messages.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">To give you something to check against: <code>2026-10-09 11:17:00<\/code> in IST is <code>1791524820000<\/code>. If you get <code>1791544620000<\/code> back, the platform treated it as UTC.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">You can&#8217;t do this on WhatsApp because there&#8217;s nothing to read back. Use the zone you found for SMS, then send yourself one scheduled WhatsApp message and check when it actually arrives on your phone.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"whatsapp-scheduling\" class=\"wp-block-heading\">WhatsApp: You Can Book It, but You Can&#8217;t Take It Back<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><code>WAApi\/send<\/code> has an optional <code>scheduletime<\/code> field. The docs describe it as &#8220;Add your schedule Timestamp in YYYY-MM-DD HH:MM format&#8221; and give <code>2026-10-03 07:13<\/code> as the example. Three things are different from SMS.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The name is all lower case. SMS uses <code>scheduleTime<\/code>, WhatsApp uses <code>scheduletime<\/code>. If you have one shared helper for both, one of them is getting the wrong parameter. Keep a small adapter per channel that knows its own field names, which is what the <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/whatsapp-business-api-wire-contract\/\">WhatsApp wire contract guide<\/a> recommends for that endpoint in general.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">There are no seconds. If your message is due at 09:29:40 and you format it as <code>%H:%M<\/code>, you&#8217;ll send <code>09:29<\/code> and it goes out twenty seconds early. Usually nobody cares. But if you&#8217;ve promised a sale opens at 09:30 sharp, or you&#8217;re right at the edge of quiet hours, round up to the next minute instead. Write that down in the code, because every date library truncates by default and someone will &#8220;fix&#8221; it later.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">And you can&#8217;t undo it. The response gives you <code>status<\/code>, <code>messageId<\/code>, <code>mobile<\/code>, a numeric <code>statusCode<\/code> and a <code>description<\/code>, and that&#8217;s the end of your involvement. There&#8217;s no endpoint to look it up, move it or cancel it. If the customer changes their mind, the template gets paused, or the recipient opts out in the meantime, the message still goes.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">So only use WhatsApp&#8217;s <code>scheduletime<\/code> when:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>the send is soon (minutes or a few hours away),<\/li>\n\n\n\n<li>the template and the values in it are final,<\/li>\n\n\n\n<li>nobody else in your system can cancel or edit it once it&#8217;s booked, and<\/li>\n\n\n\n<li>if a cancellation slipped through, it would be embarrassing rather than a compliance problem.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">If any of those aren&#8217;t true, keep the message in your own scheduler and send it normally when it&#8217;s due. You lose the &#8220;still sends if we&#8217;re down&#8221; safety net, but the hybrid setup later on gives you that back for the messages that really need it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Also keep in mind that business-initiated WhatsApp messages need an approved template, and here both <code>whatsAppStatus<\/code> and <code>systemStatus<\/code> have to be clear. A template that was approved when you booked might not be when the message goes out. If you&#8217;re sending it yourself, you can check right before. If the platform&#8217;s holding it, you&#8217;ll only find out from the <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/delivery-report-ingestion-system-of-record\/\">delivery report<\/a>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"rcs-telegram\" class=\"wp-block-heading\">RCS and Telegram: Bring Your Own Clock<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">There&#8217;s no scheduling on <code>RCSApi\/send<\/code> or <code>rest\/tg\/v1\/send<\/code>, so it&#8217;s on you. That&#8217;s not as bad as it sounds, because you need a decent scheduler for the other channels anyway, and the same one works here.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">What it needs to do:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Grab due messages safely. Something like <code>SELECT ... WHERE fire_at &lt;= now() AND state = 'scheduled' FOR UPDATE SKIP LOCKED LIMIT 100<\/code> lets several workers pull from the same table without two of them picking up the same message.<\/li>\n\n\n\n<li>Mark the row as <code>sending<\/code> in that same transaction, before you call the API. That way, if the worker crashes halfway, the message doesn&#8217;t come back as <code>scheduled<\/code> and go out twice.<\/li>\n\n\n\n<li>Guard against duplicates. RCS has an optional <code>identifier<\/code> field; put your row&#8217;s ID in it so a duplicate at least shows up in reports. Telegram&#8217;s response doesn&#8217;t give you per-recipient results, so retrying after a timeout needs some thought. The <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/message-idempotency-preventing-duplicate-sends\/\">idempotency guide<\/a> walks through it.<\/li>\n\n\n\n<li>Record when it actually went out. Keep <code>fired_at<\/code> next to <code>fire_at<\/code>. The gap is how late you were, and that&#8217;s the first number you should be alerting on.<\/li>\n\n\n\n<li>Respect the rate limits. Telegram&#8217;s bot setup has a <code>tps<\/code> value and RCS bots have their own throughput. If 50,000 messages are all due at 09:00:00, some of them are going out at 09:00:40. Decide whether that&#8217;s acceptable before a customer asks.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">One oddity on RCS: the portal&#8217;s RCS Scheduled List lets you filter by where a campaign came from, and &#8220;API&#8221; is one of the options. But there&#8217;s no API parameter for creating a scheduled RCS campaign. Go by what the send endpoint actually accepts, and if you ever see an RCS schedule that came from the API, test it before you depend on it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">On Telegram, the thing to watch for is people blocking your bot. You send to a <code>chatId<\/code> or <code>phoneNumber<\/code>, and a chat that worked when you booked the message might be blocked by the time it goes out. You&#8217;ll see that as a failed delivery in the <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/telegram-messaging-api-chat-id-model\/\">Telegram delivery report<\/a>, and you deal with it like any other failure.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"promotional-hours\" class=\"wp-block-heading\">Promotional Hours and Store and Forward<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">In India you can&#8217;t send promotional SMS whenever you like. The platform&#8217;s <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/store-forward-owh-automated-sms-compliance\/\">Store and Forward OWH announcement<\/a> says promotional SMS is blocked between 9 PM and 9 AM, and that the allowed window is &#8220;typically 9 AM to 9 PM as per TRAI guidelines&#8221;. The rules come from TRAI&#8217;s <a href=\"https:\/\/trai.gov.in\/tcccpr\" target=\"_blank\" rel=\"noopener nofollow\">Telecom Commercial Communications Customer Preference Regulations<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Store and Forward OWH (it stands for Outside Working Hours) is a switch under Profile, SMS Attributes. Here&#8217;s what it does:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>If it&#8217;s on, any promotional or service-explicit message sent outside the window gets queued and goes out when the window opens. In your SMS logs it shows as &#8220;Queued&#8221;. This works for API sends too.<\/li>\n\n\n\n<li>If it&#8217;s off, those messages &#8220;may fail or be rejected&#8221;.<\/li>\n\n\n\n<li>Transactional messages (OTPs, alerts, notifications) aren&#8217;t affected either way. They always go out immediately.<\/li>\n\n\n\n<li>You can see your account&#8217;s exact window under Profile, SMS Attributes, &#8220;Posting Hours&#8221;.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">That has some knock-on effects for scheduling.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If someone schedules a promotional message for 22:30 and OWH is on, it won&#8217;t go at 22:30. It&#8217;ll go the next time the window opens. Your database says one thing, the delivery report says another, and your user is confused. The fix is to check promotional schedules against the posting window when they&#8217;re created and either reject them or move them, so the time your user sees is the time it really goes.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Big sends near closing time can get cut in half. If a large promotional send is booked for 20:55 and takes more than five minutes to submit, the last chunk either waits until morning (OWH on) or fails (OWH off). Finish promotional sends well before closing, with enough buffer for your volume.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Don&#8217;t try to predict when a held message will go. The same announcement says a Friday 11 PM send goes out &#8220;Saturday at 9 AM&#8221; in one place and &#8220;the next business day (typically Monday)&#8221; in another. If you need the real time, read it off the delivery report.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">And time zones come up again. The window is in Indian time, so if you store UTC and haven&#8217;t pinned the platform&#8217;s zone, you can&#8217;t even tell whether 03:30 UTC (09:00 IST) is inside the window.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Message type<\/th><th>Booked inside the window<\/th><th>Outside the window, OWH on<\/th><th>Outside the window, OWH off<\/th><\/tr><\/thead><tbody><tr><td>Transactional (OTP, alerts)<\/td><td>Goes out on time<\/td><td>Goes out on time<\/td><td>Goes out on time<\/td><\/tr><tr><td>Service-explicit<\/td><td>Goes out on time<\/td><td>Held until the window opens<\/td><td>May fail or be rejected<\/td><\/tr><tr><td>Promotional<\/td><td>Goes out on time<\/td><td>Held until the window opens<\/td><td>May fail or be rejected<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"fire-time-risks\" class=\"wp-block-heading\">What Can Go Wrong While a Message Waits<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A scheduled message was written with yesterday&#8217;s information. Plenty can change before it goes out, and whether you can catch it depends on who&#8217;s holding the message.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>What changes<\/th><th>Example<\/th><th>If you&#8217;re holding it<\/th><th>If the platform&#8217;s holding it<\/th><\/tr><\/thead><tbody><tr><td>Balance<\/td><td>Wallet runs low before send time<\/td><td>Check before you send<\/td><td>Not clear when it&#8217;s charged; keep a reserve<\/td><\/tr><tr><td>DLT template<\/td><td>Deleted, edited (back into approval) or mismatched<\/td><td>Re-check the template<\/td><td>You see a failed DLR, e.g. Template Mismatch<\/td><\/tr><tr><td>Sender ID<\/td><td>Deleted or no longer linked to the template<\/td><td>Re-check senders<\/td><td>You see a failed DLR<\/td><\/tr><tr><td>WhatsApp template<\/td><td>Paused, rejected, <code>systemStatus<\/code> changed<\/td><td>Re-check the template list<\/td><td>Can&#8217;t be stopped<\/td><\/tr><tr><td>Consent<\/td><td>Person opts out<\/td><td>Check your suppression list<\/td><td>SMS: delete the booking. WhatsApp: can&#8217;t be stopped<\/td><\/tr><tr><td>Content<\/td><td>Price, stock or appointment changes<\/td><td>Rebuild the message at send time<\/td><td>SMS: delete and rebook. WhatsApp: can&#8217;t be changed<\/td><\/tr><tr><td>Posting hours<\/td><td>Window isn&#8217;t what you assumed<\/td><td>Check against the window<\/td><td>May be held or rejected<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Balance and consent are the two that cause real trouble.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">On balance: the <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/kb\/how-to-schedule-sms-messages\/\">mobile app scheduling guide<\/a> says scheduled SMS are &#8220;charged when message is sent, not when scheduled&#8221;, that cancelling early costs nothing, and that if your balance is too low at send time the message fails. The SMS pricing terms say credits come off &#8220;while sending SMS&#8221; and can&#8217;t be refunded once the message reaches the operator. So having plenty of credit when you book means nothing. Keep a running total of everything you&#8217;ve got scheduled, treat it as already spent, and alert when what&#8217;s left gets thin. The <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/reconciling-messaging-invoice-four-channels\/\">invoice reconciliation guide<\/a> shows how to read your balance and credit history through the API.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">On consent: when someone opts out, that has to stop messages that are already scheduled, not just new ones. If the message is sitting on the platform, your opt-out handler has to go and delete it, which only works for SMS and only if you saved the ID. For WhatsApp, the simple answer is to never hand off a message that someone might reasonably want to cancel.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"cancellation-race\" class=\"wp-block-heading\">The Cancel That Arrives Too Late<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Sooner or later someone hits &#8220;cancel&#8221; at the exact moment the message is being sent. You can&#8217;t stop that from happening, but you can make sure the result is clear.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If you&#8217;re holding the message, the database sorts it out. The cancel does <code>SET state = 'cancelled' WHERE id = ? AND state = 'scheduled'<\/code>, and the worker claims rows with <code>... WHERE state = 'scheduled' FOR UPDATE SKIP LOCKED<\/code>. Only one of them can win. If the cancel touches zero rows, the message is already going, and the user should see &#8220;too late to cancel&#8221;, not a cheerful &#8220;cancelled&#8221;.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If the platform&#8217;s holding an SMS, it&#8217;s your <code>SMSApi\/schedule\/delete<\/code> against the platform&#8217;s sender. Delete just says success, and nobody can tell you what it does to a message that&#8217;s already started going out. So set things up so you never need to know:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Pick a cutoff, say ten minutes before send time. Once a handed-off message is inside that window, cancelling isn&#8217;t allowed and the user is told it&#8217;s too late.<\/li>\n\n\n\n<li>Before the cutoff, call delete and then read the schedule list. If the <code>uuId<\/code> is gone, mark it cancelled. If it&#8217;s still there, try once more, then raise it with a person.<\/li>\n\n\n\n<li>After the send time, look at the delivery report for that <code>transactionId<\/code>. If there are rows, it went out, whatever delete said.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">The cutoff is the whole trick. You simply never ask the platform to cancel something that&#8217;s close enough to sending for the answer to be a coin toss.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"hybrid-scheduler\" class=\"wp-block-heading\">Putting It Together: A Hybrid Scheduler<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The whole design rests on one rule: your database is the truth about what&#8217;s scheduled. The platform&#8217;s scheduler is just something you use at the last minute, for SMS only, and you always check its work.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/10\/diagram-hybrid-scheduler-flow.svg\"><img decoding=\"async\" src=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/10\/diagram-hybrid-scheduler-flow.svg\" alt=\"Three-panel flow from your schedule table, through a handoff decision, to three provider paths with read-back verification on SMS\" class=\"wp-image-3062\"\/><\/a><figcaption class=\"wp-element-caption\">Own the clock everywhere. Hand SMS off inside a horizon and read it back. Fire RCS, Telegram and cancellable WhatsApp sends yourself.<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">There are six moving parts.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The schedule table holds every scheduled message for every channel: when it&#8217;s due (in UTC), the time zone the user picked, the channel, whether it&#8217;s transactional, service-explicit or promotional, its current state, and the platform&#8217;s ID once it has one.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The validator runs when someone creates a schedule. It says no to promotional or service-explicit messages outside posting hours, says no to WhatsApp times that need second-level precision, and makes sure the template and sender are usable right now.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The handoff worker runs every minute. It looks for SMS messages due within the handoff window (for example, somewhere between ten minutes and two hours from now). For each one it re-checks consent, the template and the balance, formats the time in the account&#8217;s zone, submits it with <code>scheduleTime<\/code>, saves the <code>transactionId<\/code> as text, and reads the booking back. Only when the <code>scheduledTimestamp<\/code> it reads back matches what it expected does the row move from <code>scheduled<\/code> to <code>handed_off<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The dispatcher runs constantly and sends anything that&#8217;s due and wasn&#8217;t handed off: all RCS and Telegram messages, WhatsApp messages (unless you&#8217;ve decided to let short-notice ones go to the platform), and any SMS whose handoff failed. It sends without a schedule parameter.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The cancel handler checks the state first. Still <code>scheduled<\/code>? Cancel it locally. <code>handed_off<\/code> and outside the cutoff? Call <code>SMSApi\/schedule\/delete<\/code> and read to confirm. Inside the cutoff or already <code>sending<\/code>? Tell the user it&#8217;s too late.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The reconciler runs every few minutes, pulls <code>SMSApi\/schedule\/read<\/code>, and compares it with everything you&#8217;ve handed off. There&#8217;s more on that below.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">You might wonder why we don&#8217;t just hand SMS to the platform as soon as it&#8217;s booked. It&#8217;s because every minute a message sits over there, things can change (consent, content, the template, your balance) and your checks won&#8217;t see them. Handing off late gives you the platform&#8217;s reliability for the last stretch and your own checks for everything before it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The lower edge of that window matters too, because it&#8217;s your cancellation cutoff. If you hand off ten minutes early, nothing in those last ten minutes can be cancelled on the platform. Say so in your UI.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"schedule-table\" class=\"wp-block-heading\">The Schedule Table<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Here&#8217;s a minimal PostgreSQL version that supports everything above:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>CREATE TABLE scheduled_message (\n    id                 BIGSERIAL PRIMARY KEY,\n    account_id         BIGINT      NOT NULL,\n    channel            TEXT        NOT NULL CHECK (channel IN ('sms','whatsapp','rcs','telegram')),\n    category           TEXT        NOT NULL CHECK (category IN ('transactional','service_explicit','promotional')),\n    fire_at            TIMESTAMPTZ NOT NULL,          -- the instant, always UTC in storage\n    user_tz            TEXT        NOT NULL,          -- IANA name the user picked, e.g. Asia\/Kolkata\n    payload            JSONB       NOT NULL,          -- template key, variables, recipients\n    state              TEXT        NOT NULL DEFAULT 'scheduled'\n                       CHECK (state IN ('scheduled','handed_off','sending','sent','cancelled','failed')),\n    provider_txn_id    TEXT,                          -- transactionId \/ uuid \/ uuId, never numeric\n    handed_off_at      TIMESTAMPTZ,\n    provider_fire_ms   BIGINT,                        -- scheduledTimestamp from read-back\n    fired_at           TIMESTAMPTZ,\n    cancel_requested_at TIMESTAMPTZ,\n    last_error         TEXT,\n    created_at         TIMESTAMPTZ NOT NULL DEFAULT now()\n);\n\nCREATE INDEX scheduled_due   ON scheduled_message (fire_at) WHERE state = 'scheduled';\nCREATE UNIQUE INDEX scheduled_provider ON scheduled_message (provider_txn_id) WHERE provider_txn_id IS NOT NULL;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Why it&#8217;s shaped like this:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>fire_at<\/code> and <code>user_tz<\/code> are separate, and there&#8217;s no local time string. If someone in a daylight-saving country books &#8220;every Monday at 10:00&#8221;, you work out each Monday&#8217;s exact time from their zone. India doesn&#8217;t change its clocks, but your customers&#8217; customers might.<\/li>\n\n\n\n<li><code>provider_txn_id<\/code> is text and unique. It&#8217;s what you join on when reconciling and when <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/delivery-report-ingestion-system-of-record\/\">ingesting delivery reports<\/a>, and the unique index catches the bug where two rows both think they own the same booking.<\/li>\n\n\n\n<li><code>provider_fire_ms<\/code> stores what the platform said, not what you sent. With both, you&#8217;ll notice if the platform ever starts reading times differently.<\/li>\n\n\n\n<li><code>cancel_requested_at<\/code> is kept apart from <code>state<\/code>, so a cancel that lost the race still leaves a trail. That&#8217;s exactly what support needs when a customer says &#8220;but I cancelled that&#8221;.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">If you already have an <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/outbound-message-table-schema-design\/\">outbound message table<\/a>, keep this one separate and link them by ID. A schedule is a plan; an outbound row is an attempt. One schedule might produce no attempts (it was cancelled) or several (a split campaign).<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"reconciliation\" class=\"wp-block-heading\">Checking Your Schedule Against the Platform<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><code>SMSApi\/schedule\/read<\/code> returns every pending SMS booking on the account, with no filters and no paging. That&#8217;s great as a cross-check and useless as your main list. Run it on a timer and sort out any differences:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Your row<\/th><th>On the platform<\/th><th>What it means<\/th><th>What to do<\/th><\/tr><\/thead><tbody><tr><td><code>handed_off<\/code>, still in the future<\/td><td>There, same <code>scheduledTimestamp<\/code><\/td><td>All good<\/td><td>Nothing<\/td><\/tr><tr><td><code>handed_off<\/code>, still in the future<\/td><td>There, different <code>scheduledTimestamp<\/code><\/td><td>Someone moved it outside your system (portal, another integration)<\/td><td>Alert and decide which side wins<\/td><\/tr><tr><td><code>handed_off<\/code>, still in the future<\/td><td>Missing<\/td><td>Cancelled elsewhere, or never booked<\/td><td>Alert, check delivery reports before resending<\/td><\/tr><tr><td><code>handed_off<\/code>, time has passed<\/td><td>Missing<\/td><td>It went out<\/td><td>Wait for delivery reports to mark it <code>sent<\/code><\/td><\/tr><tr><td><code>handed_off<\/code>, time has passed<\/td><td>Still there<\/td><td>It&#8217;s late or stuck<\/td><td>Alert on lateness<\/td><\/tr><tr><td>No row on your side<\/td><td>There<\/td><td>Someone else booked it on this account<\/td><td>Alert, then adopt or delete by policy<\/td><\/tr><tr><td><code>cancelled<\/code><\/td><td>Still there<\/td><td>Your delete didn&#8217;t stick<\/td><td>Retry, escalate if it keeps happening<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">A few things to keep in mind.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Compare IDs as text. Match <code>uuId<\/code> to <code>provider_txn_id<\/code> as strings. Only turn <code>scheduledTimestamp<\/code> into a number for the time comparison; at thirteen digits it fits safely in any 64-bit integer or double.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Lower-case the status. The API sample says <code>\"pending\"<\/code>, while the portal&#8217;s <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/kb\/what-statuses-might-i-see-for-scheduled-messages\/\">scheduled message statuses<\/a> are Pending, Sent, Failed and Processing.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Never resend something just because it&#8217;s missing. If it&#8217;s missing and past due, it almost certainly went out. If it&#8217;s missing and still in the future, someone probably cancelled it on purpose. Either way, a person should decide before anything gets sent again.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">And expect bookings you didn&#8217;t make. The portal, the mobile app and other integrations can all schedule on the same account, and the read call shows all of it. Those rows aren&#8217;t errors, just things to know about.<\/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\">Code: Four Samples, Four Gotchas<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Each of these shows one mistake that&#8217;s easy to make. They all handle responses the same way: parse loosely, look only at <code>status<\/code> wherever that endpoint puts it, stop if it isn&#8217;t a success, and only then read the rest. None of them look at <code>code<\/code> or <code>statusCode<\/code>, and none of them try to parse <code>msg<\/code> or <code>reason<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Python: book it, save the ID, then prove it worked<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The gotcha: believing the send response. A booking only counts once the read endpoint shows it at the time you expected.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import requests\nfrom datetime import datetime\nfrom zoneinfo import ZoneInfo\n\nBASE = \"https:\/\/unify.smsgateway.center\"\n\ndef book_sms(session, userid, apikey, pinned_tz, fire_at_utc, params):\n    local = fire_at_utc.astimezone(ZoneInfo(pinned_tz))\n    data = dict(params, userid=userid, output=\"json\",\n                scheduleTime=local.strftime(\"%Y-%m-%d %H:%M:%S\"))\n    r = session.post(f\"{BASE}\/SMSApi\/send\", data=data,\n                     headers={\"apikey\": apikey}, timeout=30)\n    body = r.json()\n    if body.get(\"status\") != \"success\":          # send puts status at top level\n        return None, body\n    txn = str(body&#91;\"transactionId\"])              # keep as text\n    return txn, body\n\ndef verify_booking(session, userid, apikey, txn, fire_at_utc):\n    r = session.post(f\"{BASE}\/SMSApi\/schedule\/read\",\n                     data={\"userid\": userid, \"output\": \"json\"},\n                     headers={\"apikey\": apikey}, timeout=30)\n    resp = r.json().get(\"response\", {})\n    if resp.get(\"status\") != \"success\":           # schedule family nests it\n        return False\n    expected = int(fire_at_utc.timestamp()) * 1000\n    for item in resp.get(\"scheduleList\") or &#91;]:\n        row = item.get(\"schedule\", {})            # each row is wrapped\n        if str(row.get(\"uuId\")) == txn:\n            return int(row.get(\"scheduledTimestamp\", \"0\")) == expected\n    return False<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Save <code>txn<\/code> to your row before calling <code>verify_booking<\/code>. If the check fails, the row stays <code>scheduled<\/code> with the ID filled in, and the reconciler decides whether to delete the platform booking and let your own dispatcher send it instead.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Node.js: read the schedule list without mangling it<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The gotcha: letting <code>uuId<\/code> turn into a <code>Number<\/code>, and treating <code>\"0\"<\/code> as a real date.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>async function readSchedules(userid, apikey) {\n  const res = await fetch(\"https:\/\/unify.smsgateway.center\/SMSApi\/schedule\/read\", {\n    method: \"POST\",\n    headers: { apikey, \"content-type\": \"application\/x-www-form-urlencoded\" },\n    body: new URLSearchParams({ userid, output: \"json\" }),\n  });\n  const text = await res.text();\n  let parsed;\n  try { parsed = JSON.parse(text); } catch { return { ok: false, raw: text }; }\n  const r = parsed &amp;&amp; parsed.response;\n  if (!r || r.status !== \"success\") return { ok: false, raw: parsed };\n\n  const list = Array.isArray(r.scheduleList) ? r.scheduleList : &#91;];\n  return {\n    ok: true,\n    rows: list.map(({ schedule: s = {} }) =&gt; ({\n      providerTxnId: String(s.uuId),                    \/\/ compare as text only\n      status: String(s.status || \"\").toLowerCase(),\n      recipients: Number.parseInt(s.total, 10),\n      createdMs: Number(s.timestamp),                    \/\/ 13 digits: safe\n      fireMs: Number(s.scheduledTimestamp),\n      updatedMs: s.lastupdatedTimestamp === \"0\" ? null : Number(s.lastupdatedTimestamp),\n    })),\n  };\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This only keeps <code>uuId<\/code> intact because the API sends it in quotes. <code>String(s.uuId)<\/code> is a seatbelt, not a fix; if a value ever arrives without quotes, the damage is done before this line runs. The <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/contract-testing-harness-messaging-api\/\">contract testing guide<\/a> shows how to catch that at parse time and how to get a nightly alert if a quoted field changes type.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Java: format a WhatsApp time without sending early<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The gotcha: no seconds. Truncating sends early, so round up.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import java.time.*;\nimport java.time.format.DateTimeFormatter;\nimport java.time.temporal.ChronoUnit;\n\npublic final class WhatsAppSchedule {\n    private static final DateTimeFormatter WA = DateTimeFormatter.ofPattern(\"yyyy-MM-dd HH:mm\");\n\n    \/** Returns the scheduletime value, or null if this send must stay in our own dispatcher. *\/\n    public static String format(Instant fireAt, ZoneId pinnedZone, boolean cancellable, Duration horizon) {\n        if (cancellable) return null;                                   \/\/ no cancel endpoint exists\n        if (Duration.between(Instant.now(), fireAt).compareTo(horizon) &gt; 0) return null;\n        Instant rounded = fireAt.truncatedTo(ChronoUnit.MINUTES);\n        if (rounded.isBefore(fireAt)) rounded = rounded.plus(1, ChronoUnit.MINUTES);  \/\/ never early\n        return WA.format(rounded.atZone(pinnedZone));\n    }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Send the result as the form field <code>scheduletime<\/code>, all lower case. If it returns <code>null<\/code>, the message stays with your own dispatcher and goes out on time without the parameter. The &#8220;should we hand this off?&#8221; decision lives in one function, with the reasons right there in the code.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">PHP: check a promotional send against posting hours<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The gotcha: PHP&#8217;s default time zone. On most cloud servers it&#8217;s UTC, so <code>new DateTime('21:00')<\/code> actually means 02:30 IST the next day.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;?php\nfunction validatePromotionalSchedule(DateTimeImmutable $fireAtUtc, int $recipients,\n                                     int $sendsPerMinute, string $open = '09:00',\n                                     string $close = '21:00'): ?string {\n    $ist   = new DateTimeZone('Asia\/Kolkata');          \/\/ explicit, never the server default\n    $local = $fireAtUtc-&gt;setTimezone($ist);\n    $day   = $local-&gt;format('Y-m-d');\n    $start = new DateTimeImmutable(\"$day $open:00\", $ist);\n    $end   = new DateTimeImmutable(\"$day $close:00\", $ist);\n\n    if ($local &lt; $start || $local &gt;= $end) {\n        return 'outside posting hours';\n    }\n    $minutesNeeded = (int) ceil($recipients \/ max(1, $sendsPerMinute));\n    $finish = $local-&gt;modify(\"+{$minutesNeeded} minutes\");\n    if ($finish &gt; $end-&gt;modify('-15 minutes')) {\n        return 'too close to closing time for this volume';\n    }\n    return null;                                          \/\/ valid\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Take the open and close times from the account&#8217;s Posting Hours rather than hard-coding them, and base <code>sendsPerMinute<\/code> on throughput you&#8217;ve actually measured. The fifteen-minute buffer is a judgement call; pick something that covers your slowest real-world send rate.<\/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\">Which Path Should This Message Take?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">When you&#8217;re not sure where a particular message should be sent from, this table should settle it.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>If the message is&#8230;<\/th><th>Send it from<\/th><th>Because<\/th><\/tr><\/thead><tbody><tr><td>SMS, transactional, more than two hours away<\/td><td>Your scheduler, then hand off inside the window<\/td><td>You keep your checks for most of the wait and get the platform&#8217;s reliability at the end<\/td><\/tr><tr><td>SMS, due soon, content final<\/td><td>Hand off now with <code>scheduleTime<\/code><\/td><td>SMS can be read, moved and cancelled; read it back to confirm<\/td><\/tr><tr><td>SMS that the user might keep editing until the last minute<\/td><td>Your scheduler<\/td><td>Update only changes the time; editing the text means delete and rebook<\/td><\/tr><tr><td>An SMS split campaign built in the portal<\/td><td>The platform, moved with <code>SMSApi\/campaign\/update<\/code><\/td><td>Campaigns are created in the portal; mirror them in your table by <code>campaignid<\/code><\/td><\/tr><tr><td><a href=\"https:\/\/www.smsgatewaycenter.com\/promotional-sms\/\">Promotional SMS<\/a> booked outside posting hours<\/td><td>Nowhere: reject it when it&#8217;s booked<\/td><td>Otherwise it gets held or rejected and the time you showed the user is wrong<\/td><\/tr><tr><td><a href=\"https:\/\/www.smsgatewaycenter.com\/whatsapp-business-api\/\">WhatsApp<\/a>, due soon, final, nobody can cancel it<\/td><td>Hand off with <code>scheduletime<\/code>, rounded up<\/td><td>Fine, because nothing can change<\/td><\/tr><tr><td>WhatsApp that anyone might cancel or edit<\/td><td>Your scheduler<\/td><td>There&#8217;s no read, update or delete for WhatsApp bookings<\/td><\/tr><tr><td>WhatsApp whose template might change before sending<\/td><td>Your scheduler<\/td><td>You can re-check <code>whatsAppStatus<\/code> and <code>systemStatus<\/code> right before sending<\/td><\/tr><tr><td>RCS, any time<\/td><td>Your scheduler<\/td><td>There&#8217;s no API schedule parameter<\/td><\/tr><tr><td>Telegram, any time<\/td><td>Your scheduler<\/td><td>There&#8217;s no API schedule parameter<\/td><\/tr><tr><td>Someone opts out and has messages pending<\/td><td>Cancel locally, delete any handed-off SMS<\/td><td>Only SMS bookings can be removed from the platform<\/td><\/tr><tr><td>Your dispatcher goes down at send time<\/td><td>Handed-off SMS and WhatsApp still go; the rest are late<\/td><td>Size the handoff window to cover the outages you actually expect for critical traffic<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"testing-scheduler\" class=\"wp-block-heading\">Testing a Scheduler Without Waiting for the Clock<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Schedulers are a pain to test because the interesting stuff happens at times you don&#8217;t want to sit and wait for. So separate the parts that need a real clock from the parts that don&#8217;t.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Pass the clock in. Every part of the system should ask an injected clock what time it is, never the system directly. In tests you just move it forward. That covers the validator, the handoff window, the cutoff and the dispatcher&#8217;s query without a single <code>sleep<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Test the formatting with real edge cases. Give each channel adapter a list of times and the exact strings you expect back, and include the ones that break naive code: a WhatsApp time with seconds (has to round up), a time that crosses midnight once converted to IST, one second before the posting window closes, and something due on 31 December that tips into the next year.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Test the cancel race properly. Fire off two transactions at once, one cancelling and one claiming the same row, and check that exactly one wins. Do it a few hundred times in CI, because it won&#8217;t fail every time.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Check against the real platform now and then. Once a week on a staging account is plenty: run the time zone test, do a full book, read, move, read, delete, read cycle on SMS, and send yourself one scheduled WhatsApp message. Book things far in the future and delete them at the end so nothing actually gets delivered. You might be tempted to use <code>testMessage=true<\/code> on the SMS send, which does stop delivery, but you need a booking that shows up in the schedule list, so book it for real, delete it, and confirm the list is empty afterwards.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Watch for the API changing shape. The <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/contract-testing-harness-messaging-api\/\">contract testing harness<\/a> approach works well here: fingerprint the <code>SMSApi\/schedule\/read<\/code> response every night and get an alert if <code>uuId<\/code> stops being a string, the <code>schedule<\/code> wrapper disappears, or <code>count<\/code> changes type.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Measure lateness in production. Track <code>fired_at - fire_at<\/code> per channel as a histogram. If it creeps up through the day, your dispatcher needs more capacity. If it jumps at 21:00 and clears at 09:00, promotional messages are being held, which means something slipped past your validator. The <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/observability-for-messaging-pipelines\/\">observability guide<\/a> shows how to turn that into an alert that warns you early instead of after the fact.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"checklist\" class=\"wp-block-heading\">Before You Go Live<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Every schedule is saved as a UTC time plus an IANA time zone name, never as a local time string.<\/li>\n\n\n\n<li>You&#8217;ve run the time zone test for this account instead of assuming IST.<\/li>\n\n\n\n<li>Each channel has its own adapter that knows its parameter name (<code>scheduleTime<\/code> or <code>scheduletime<\/code>) and format (with or without seconds).<\/li>\n\n\n\n<li>WhatsApp times round up to the next minute.<\/li>\n\n\n\n<li><code>transactionId<\/code>, <code>uuid<\/code> and <code>uuId<\/code> all map to one field, stored as text, with a unique index.<\/li>\n\n\n\n<li>A message only counts as handed off once <code>SMSApi\/schedule\/read<\/code> shows the right <code>scheduledTimestamp<\/code>.<\/li>\n\n\n\n<li>RCS and Telegram messages are sent by your own dispatcher.<\/li>\n\n\n\n<li>Any WhatsApp message that could be cancelled or edited is sent by your own dispatcher.<\/li>\n\n\n\n<li>Promotional and service-explicit schedules are checked against the account&#8217;s Posting Hours when they&#8217;re created, with a buffer before closing time.<\/li>\n\n\n\n<li>You know whether Store and Forward OWH is on for this account, and it&#8217;s recorded somewhere.<\/li>\n\n\n\n<li>Balance, template, sender and opt-out checks happen right before sending.<\/li>\n\n\n\n<li>Everything scheduled but not yet sent is counted against your balance.<\/li>\n\n\n\n<li>Cancelling is a conditional update, and losing the race shows &#8220;too late&#8221;, not &#8220;cancelled&#8221;.<\/li>\n\n\n\n<li>Cancelling a handed-off SMS calls <code>SMSApi\/schedule\/delete<\/code> and then reads the list to confirm.<\/li>\n\n\n\n<li>There&#8217;s a cancellation cutoff, and users can see it.<\/li>\n\n\n\n<li>Opting out cancels pending messages on every channel.<\/li>\n\n\n\n<li>A reconciler compares your handed-off rows with <code>SMSApi\/schedule\/read<\/code> and never resends anything on its own.<\/li>\n\n\n\n<li>Lateness is measured per channel and has an alert.<\/li>\n\n\n\n<li>Tests with an injected clock cover midnight, year end, the edges of the posting window and the cancel race.<\/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\">There are a few things here you just can&#8217;t find out by reading before you build. For each one, below is the choice that works whichever way the platform actually behaves, so you don&#8217;t have to wait on an answer to ship.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>One. Measure the time zone, and switch scheduling off if the test doesn&#8217;t match.<\/strong> No schedule input carries a time zone, and the portal talks about both an account zone and a server time. The test booking turns that into a stored fact for each account. If nothing matches, don&#8217;t fall back to a default. A wrong zone moves every message by hours; a disabled feature doesn&#8217;t move anything.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Two. Only trust a booking once you&#8217;ve read it back.<\/strong> A scheduled SMS gets the same send response as a normal one. Look it up in <code>SMSApi\/schedule\/read<\/code> by <code>uuId<\/code>. If you always confirm by reading, it doesn&#8217;t matter whether the send response ever starts looking different for scheduled messages.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Three. Use each endpoint&#8217;s own spelling of the schedule field.<\/strong> Send uses <code>scheduleTime<\/code>; update, campaign update and WhatsApp use <code>scheduletime<\/code>, and one sample on the SMS send page uses the lower-case version too. Don&#8217;t count on the names being case-insensitive. Matching each endpoint exactly works either way.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Four. Only hand WhatsApp the messages nobody will want back.<\/strong> You can&#8217;t read, move or cancel a WhatsApp booking through the API. Keep anything cancellable in your own scheduler and that gap stops mattering.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Five. Round WhatsApp times up, and keep the seconds on your side.<\/strong> WhatsApp takes minutes, you store seconds. Rounding up means you&#8217;re never early and at most fifty-nine seconds late, which is the right way to be wrong for any &#8220;not before&#8221; promise.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Six. Never ask the platform to cancel inside your cutoff.<\/strong> Nobody can tell you what <code>SMSApi\/schedule\/delete<\/code> does to a message that&#8217;s already started sending. With a cutoff you never have to ask, and the user gets a clear &#8220;too late&#8221; instead of a cancel that might or might not have worked.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Seven. Hold back enough balance for everything you&#8217;ve scheduled.<\/strong> One help page says scheduled SMS are charged when they&#8217;re sent; the pricing terms say credits come off while sending. Whether anything is reserved at booking time is unclear. Treating your pending schedule as already spent covers you both ways.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Eight. Don&#8217;t guess when a held promotional message will go out.<\/strong> The Store and Forward announcement gives two different answers for a Friday night send. Keep promotional schedules inside posting hours so nothing gets held in the first place, and for anything that does, take the real time from the delivery report.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Nine. Keep a note of each account&#8217;s Store and Forward setting and posting hours.<\/strong> You can&#8217;t read either through the API. Store them as account settings, filled in when the account is set up, and show them in your admin screen. A scheduler that knows the window can avoid it; one that doesn&#8217;t only finds out from delivery reports.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Ten. Call <code>SMSApi\/schedule\/read<\/code> on a timer, and keep an eye on how big it gets.<\/strong> It has no paging. Don&#8217;t call it on every request, and alert if the row count grows past what you&#8217;d expect. If it ever gets cut off, the missing rows look absent to your reconciler, which, per the rule above, means an alert, not a resend.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Eleven. Lower-case the schedule status and only act on values you&#8217;ve seen.<\/strong> The API sample shows <code>pending<\/code>; the portal shows Pending, Sent, Failed and Processing. Act on what you recognise, log and skip the rest. An unexpected value then delays a decision instead of causing a wrong one.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Twelve. Track split campaigns by <code>campaignid<\/code> plus each row&#8217;s <code>uuId<\/code>.<\/strong> The campaign read sample has the same row shape as the schedule read sample, with no <code>campaignid<\/code> in the row. Combining the <code>campaignid<\/code> you asked for with each row&#8217;s <code>uuId<\/code> gives you a stable key whether or not that field ever turns up.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 id=\"faqs\" class=\"wp-block-heading\">FAQs<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Can I schedule an SMS through the SMSGatewayCenter API?<\/strong><br>Yes. Add <code>scheduleTime<\/code> (<code>YYYY-MM-DD HH:MM:SS<\/code>) to <code>SMSApi\/send<\/code>. You can then list pending sends with <code>SMSApi\/schedule\/read<\/code>, move one with <code>SMSApi\/schedule\/update<\/code> (<code>uuid<\/code>, <code>scheduletime<\/code>) and cancel with <code>SMSApi\/schedule\/delete<\/code> (<code>uuid<\/code>).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Can I schedule a WhatsApp message through the API?<\/strong><br>Yes, using <code>scheduletime<\/code> (<code>YYYY-MM-DD HH:MM<\/code>) on <code>WAApi\/send<\/code>. Just be aware you can&#8217;t look it up, move it or cancel it afterwards, so only do it for messages that definitely won&#8217;t change.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>What about RCS and Telegram?<\/strong><br>Not through the API. Neither <code>RCSApi\/send<\/code> nor <code>rest\/tg\/v1\/send<\/code> takes a schedule time. You can schedule RCS campaigns in the portal, but if you&#8217;re integrating, store the schedule yourself and send when it&#8217;s due.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Which time zone does the schedule time use?<\/strong><br>The API doesn&#8217;t say, so find out. Book one test send, read its <code>scheduledTimestamp<\/code> from <code>SMSApi\/schedule\/read<\/code>, and compare it with your timestamp converted as UTC and as Asia\/Kolkata. Whichever matches is your answer.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Why are the times in the read response long numbers in quotes?<\/strong><br>They&#8217;re epoch milliseconds sent as strings. Convert them once when you read them. A <code>lastupdatedTimestamp<\/code> of <code>\"0\"<\/code> just means the booking has never been moved.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Why does the ID have three different names?<\/strong><br>It&#8217;s <code>transactionId<\/code> when you send, <code>uuid<\/code> when you update or delete, and <code>uuId<\/code> when you read. Same value each time. Give it one name in your own code and keep it as text.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Why can&#8217;t I store the transaction ID as a number?<\/strong><br>It can be nineteen digits, which is more than a double can hold exactly. Convert it and the last few digits change, so any cancel you send with it will miss.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Can I change the text of a scheduled SMS?<\/strong><br>No, update only changes the time. Delete it and book a new one, or keep the message in your own scheduler until it&#8217;s final.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>What happens if I schedule a promotional SMS for 10 PM?<\/strong><br>It&#8217;s outside the posting window (usually 9 AM to 9 PM). With Store and Forward OWH on, it waits and goes out when the window opens. With it off, it may fail or be rejected. Transactional SMS isn&#8217;t affected.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Do I get charged when I schedule or when it sends?<\/strong><br>The platform&#8217;s help content says you&#8217;re charged when it sends, and cancelling before then costs nothing. Make sure there&#8217;s enough balance for everything you&#8217;ve scheduled, because if there isn&#8217;t when it&#8217;s time to send, the message fails.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>How do I cancel scheduled messages when someone opts out?<\/strong><br>Cancel anything pending in your own scheduler, call <code>SMSApi\/schedule\/delete<\/code> for any SMS you&#8217;ve already handed to the platform, and read the list to confirm. You can&#8217;t cancel WhatsApp bookings through the API, which is why cancellable WhatsApp messages should stay in your own scheduler.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>How early should I hand a message to the platform?<\/strong><br>Late. Somewhere between a few minutes and a few hours before it&#8217;s due means your own checks on consent, templates and balance cover most of the wait, and the platform still sends it if your workers fall over at the last moment.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>How do I move a split campaign?<\/strong><br>Use <code>SMSApi\/campaign\/update<\/code> with <code>campaignid<\/code> and <code>scheduletime<\/code> (<code>YYYY-MM-DD HH:MM:SS<\/code>). The campaigns themselves are set up in the portal.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>How do I know a scheduled message actually went out?<\/strong><br>Check the delivery reports for its transaction ID. The schedule list only shows what&#8217;s still waiting; once a message goes, it drops off that list.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Set up scheduling once and use it everywhere<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Create a free SMSGatewayCenter account, try the time zone test against your own number in the <a href=\"https:\/\/www.smsgatewaycenter.com\/demo\/\">Sandbox<\/a>, and wire the schedule endpoints into your dispatcher before your first campaign. Planning scheduled traffic across SMS, WhatsApp, RCS and Telegram and want to talk through volumes and posting hours? <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\">Previous Posts<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><a 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><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><a 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><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><a 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><\/p>\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 SMS, RCS, WhatsApp and Telegram: One API Comparison<\/a><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><a 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><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n","protected":false},"excerpt":{"rendered":"<p>You can schedule an SMS through the API and change your mind later. WhatsApp lets you book but never undo. RCS and Telegram don&#8217;t let you schedule at all. Here&#8217;s how each one really works, how to find out which time zone your timestamps land in, and how to build one scheduler that behaves the same on all four.<\/p>\n","protected":false},"author":118,"featured_media":3060,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2010],"tags":[2296,811,2295,2298,2297,481,1774,2299,2300,256,632],"class_list":["post-3059","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-developer-guides","tag-message-scheduling","tag-rcs-messaging","tag-schedule-whatsapp-message-api","tag-scheduled-sms-api","tag-scheduler-design","tag-sms-api","tag-store-and-forward","tag-telegram-bot-api","tag-time-zones","tag-trai","tag-whatsapp-business-api"],"_links":{"self":[{"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/posts\/3059","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=3059"}],"version-history":[{"count":0,"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/posts\/3059\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/media\/3060"}],"wp:attachment":[{"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/media?parent=3059"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/categories?post=3059"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/tags?post=3059"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}