{"id":3119,"date":"2026-10-26T16:27:49","date_gmt":"2026-10-26T10:57:49","guid":{"rendered":"https:\/\/www.smsgatewaycenter.com\/blog\/?p=3119"},"modified":"2026-10-26T16:27:51","modified_gmt":"2026-10-26T10:57:51","slug":"voice-call-api-wire-contract-obd-otp-delivery-reports","status":"publish","type":"post","link":"https:\/\/www.smsgatewaycenter.com\/blog\/voice-call-api-wire-contract-obd-otp-delivery-reports\/","title":{"rendered":"Voice Call API Wire Contract: OBD Calls, Voice OTP, Audio Library, Schedules and Delivery Reports"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">A field-level guide to the <strong>SMSGatewayCenter Voice Call API<\/strong>: fourteen operations across nine paths, one call identifier spelled four ways, nineteen-digit IDs that break doubles, an OTP <code>pin<\/code> typed as an integer, platform redial versus your own retries, and how to page delivery reports when the request has no page parameter.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/10\/voice-call-api-wire-contract-featured.webp\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"584\" src=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/10\/voice-call-api-wire-contract-featured-1024x584.webp\" alt=\"Abstract teal sound waveform passing through blue hexagonal nodes and fanning out into orange lines on a navy background\" class=\"wp-image-3120\" srcset=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/10\/voice-call-api-wire-contract-featured-1024x584.webp 1024w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/10\/voice-call-api-wire-contract-featured-300x171.webp 300w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/10\/voice-call-api-wire-contract-featured-768x438.webp 768w, https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/10\/voice-call-api-wire-contract-featured.webp 1200w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><figcaption class=\"wp-element-caption\">One voice call, from a single API request to many delivery report rows.<\/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=\"#operations\">The Fourteen Operations at a Glance<\/a><\/li>\n\n\n\n<li><a href=\"#auth-encoding\">Authentication and Request Encoding<\/a><\/li>\n\n\n\n<li><a href=\"#sending-calls\">Sending a Call: quick, group and bulkupload<\/a><\/li>\n\n\n\n<li><a href=\"#voice-otp\">Voice OTP: the PIN Send<\/a><\/li>\n\n\n\n<li><a href=\"#audio-library\">The Audio Library: Upload, IDs and Renewal<\/a><\/li>\n\n\n\n<li><a href=\"#four-identifiers\">One Call, Four Identifiers<\/a><\/li>\n\n\n\n<li><a href=\"#big-numbers\">Big Numbers and the Types That Break Them<\/a><\/li>\n\n\n\n<li><a href=\"#redial\">Redial: Who Owns the Retry<\/a><\/li>\n\n\n\n<li><a href=\"#scheduling\">Scheduling, Moving and Cancelling Calls<\/a><\/li>\n\n\n\n<li><a href=\"#delivery-reports\">Reading Delivery Reports<\/a><\/li>\n\n\n\n<li><a href=\"#dlr-fields\">What a Voice DLR Row Tells You<\/a><\/li>\n\n\n\n<li><a href=\"#paging\">Paging Without a Page Parameter<\/a><\/li>\n\n\n\n<li><a href=\"#summary-archive\">Summary, Upload History and the Daily Archive<\/a><\/li>\n\n\n\n<li><a href=\"#time-zones\">Time Zones and Date Formats<\/a><\/li>\n\n\n\n<li><a href=\"#client-samples\">Four Client Samples, Four Traps<\/a><\/li>\n\n\n\n<li><a href=\"#data-model\">A Data Model for Voice Calls<\/a><\/li>\n\n\n\n<li><a href=\"#checklist\">Production Checklist<\/a><\/li>\n\n\n\n<li><a href=\"#unspecified-behaviour\">Unspecified Behaviour and How to Code Around It<\/a><\/li>\n\n\n\n<li><a href=\"#faqs\">FAQs<\/a><\/li>\n<\/ol>\n\n\n\n<h2 id=\"the-short-answer\" class=\"wp-block-heading\">The Short Answer<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The SMSGatewayCenter <a href=\"https:\/\/www.smsgatewaycenter.com\/bulk-voice-call\/\">Voice Call<\/a> API lives under <code>https:\/\/unify.smsgateway.center\/VoiceApi\/<\/code> and covers fourteen operations on nine paths: sending a call (<code>VoiceApi\/send<\/code> with <code>sendMethod<\/code> set to <code>quick<\/code>, <code>group<\/code> or <code>bulkupload<\/code>), sending a voice OTP (<code>VoiceApi\/send?sendMethod=pin<\/code>), uploading and renewing audio clips (<code>VoiceApi\/save<\/code>), reading, moving and cancelling scheduled calls, and five kinds of report, from per-call delivery rows to a zipped daily archive.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Getting a call to ring is the easy part. The parts that bite in production are all about data:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>One call carries four identifiers: <code>transactionId<\/code> in the send reply, <code>uuid<\/code> in schedule requests, <code>uuId<\/code> in report rows, and a per-call <code>msgId<\/code> in delivery rows.<\/li>\n\n\n\n<li>Those IDs are 18 or 19 digits long. Several are typed as integers in the reference, and any language that parses JSON numbers into doubles will silently change them.<\/li>\n\n\n\n<li>The voice OTP <code>pin<\/code> is typed as an integer, so an OTP that starts with zero can lose a digit before it&#8217;s ever spoken.<\/li>\n\n\n\n<li>The platform redials unanswered calls for you (<code>reDial<\/code> 0, 1 or 2, <code>redialInterval<\/code> 5, 10 or 15 minutes). If your code also retries, one person can get four or more calls.<\/li>\n\n\n\n<li>The report endpoints return cursors and page counts, but the request schemas have no page parameter, so you control result size by narrowing the date window.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Store every identifier as text, send the PIN as a string, pick one owner for retries, and page by splitting date ranges. The rest of this guide goes through each endpoint field by field and gives you code that holds up whichever way the open questions resolve.<\/p>\n\n\n\n<h2 id=\"tldr\" class=\"wp-block-heading\">TL;DR<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Base URL:<\/strong> <code>https:\/\/unify.smsgateway.center<\/code>. The reference is an OpenAPI 3.0 file rendered as Swagger on the <a href=\"https:\/\/www.smsgatewaycenter.com\/developer-api\/voice-call-api\/\">Voice Call API page<\/a>.<\/li>\n\n\n\n<li><strong>Auth:<\/strong> <code>userid<\/code> plus <code>password<\/code>, or <code>userid<\/code> plus the <code>apikey<\/code> header. Use the header. <code>userid<\/code> still goes in the request.<\/li>\n\n\n\n<li><strong>Encoding:<\/strong> POST endpoints take <code>multipart\/form-data<\/code>. Every form field is text on the wire, whatever type the schema gives it.<\/li>\n\n\n\n<li><strong>Send reply:<\/strong> <code>{status, mobile, invalidMobile, transactionId, statusCode, reason}<\/code>, with <code>transactionId<\/code> as a quoted 19-digit string and <code>statusCode<\/code> as the string <code>\"200\"<\/code>.<\/li>\n\n\n\n<li><strong>Voice OTP:<\/strong> <code>pin<\/code> and <code>pinClipId<\/code> with a GET to <code>VoiceApi\/send?sendMethod=pin<\/code>. Always send <code>pin<\/code> as a string so leading zeros survive.<\/li>\n\n\n\n<li><strong>Audio library:<\/strong> <code>VoiceApi\/save<\/code> uploads an mp3 and returns <code>libraryId<\/code> as a string. <code>VoiceApi\/save?reUpload=true<\/code> renews an expired clip. There&#8217;s no endpoint to list clips, so keep your own registry.<\/li>\n\n\n\n<li><strong>Redial:<\/strong> at most two platform redials, at most 15 minutes apart. Worst case, the last attempt starts 30 minutes after the first. Make sure a voice OTP is still valid by then, or set <code>reDial<\/code> to 0.<\/li>\n\n\n\n<li><strong>Scheduling:<\/strong> <code>scheduleTime<\/code> on send, then <code>report\/schedule<\/code>, <code>report\/schedulelog<\/code>, <code>updateSchedule<\/code> and <code>cancelSchedule<\/code>, all keyed by <code>uuid<\/code>.<\/li>\n\n\n\n<li><strong>Reports:<\/strong> <code>report\/dlr<\/code> by date range, by <code>uuid<\/code> (<code>?tId=true<\/code>) or by mobile (<code>?isMob=true<\/code>); <code>report\/summary<\/code> for daily buckets; <code>report\/uploadhistory<\/code> for CSV uploads; <code>report\/day<\/code> for a zipped archive.<\/li>\n\n\n\n<li><strong>Rows:<\/strong> delivery rows sit directly in <code>response.reports_dlrList<\/code>, not wrapped in an extra object the way several <code>SMSApi\/<\/code> lists are.<\/li>\n\n\n\n<li><strong>Types:<\/strong> treat <code>msgId<\/code>, <code>uuId<\/code>, <code>transactionId<\/code>, <code>libraryId<\/code>, <code>mobileNo<\/code> and <code>pin<\/code> as strings in your code and your database.<\/li>\n\n\n\n<li><strong>Paging:<\/strong> no page parameter is defined on any report request. Keep each window small enough that <code>count.total<\/code> equals the rows you got back, and split it when it isn&#8217;t.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"operations\" class=\"wp-block-heading\">The Fourteen Operations at a Glance<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Here&#8217;s every operation in the reference, with the method each one is defined under and the key it&#8217;s addressed by.<\/p>\n\n\n\n<figure><div class=\"table-responsive\"><table class=\"table table-striped table-bordered table-hover\"><thead><tr><th>#<\/th><th>Operation<\/th><th>Method and path<\/th><th>Addressed by<\/th><th>Main reply payload<\/th><\/tr><\/thead><tbody><tr><td>1<\/td><td>Send a library call<\/td><td>POST <code>VoiceApi\/send<\/code><\/td><td><code>libraryId<\/code>, <code>sendMethod<\/code><\/td><td>flat <code>{status, transactionId, ...}<\/code><\/td><\/tr><tr><td>2<\/td><td>Send a voice OTP<\/td><td>GET <code>VoiceApi\/send?sendMethod=pin<\/code><\/td><td><code>pin<\/code>, <code>pinClipId<\/code><\/td><td>flat <code>{status, transactionId, ...}<\/code><\/td><\/tr><tr><td>3<\/td><td>Upload a clip<\/td><td>POST <code>VoiceApi\/save<\/code><\/td><td><code>libraryName<\/code>, <code>audioTrack<\/code><\/td><td>flat <code>{status, length, frequency, libraryId, ...}<\/code><\/td><\/tr><tr><td>4<\/td><td>Renew a clip<\/td><td>GET <code>VoiceApi\/save?reUpload=true<\/code><\/td><td><code>libraryId<\/code><\/td><td>same as upload<\/td><\/tr><tr><td>5<\/td><td>List a scheduled transaction<\/td><td>POST <code>VoiceApi\/report\/schedule<\/code><\/td><td><code>uuid<\/code><\/td><td><code>response.reports_scheduleList[]<\/code><\/td><\/tr><tr><td>6<\/td><td>List the calls in a schedule<\/td><td>POST <code>VoiceApi\/report\/schedulelog<\/code><\/td><td><code>uuid<\/code><\/td><td><code>response.reports_scheduleList[]<\/code><\/td><\/tr><tr><td>7<\/td><td>Move a schedule<\/td><td>POST <code>VoiceApi\/updateSchedule<\/code><\/td><td><code>uuid<\/code>, <code>scheduleTime<\/code><\/td><td>flat <code>{status, transactionId, ...}<\/code><\/td><\/tr><tr><td>8<\/td><td>Cancel a schedule<\/td><td>POST <code>VoiceApi\/cancelSchedule<\/code><\/td><td><code>uuid<\/code><\/td><td>flat <code>{status, transactionId, ...}<\/code><\/td><\/tr><tr><td>9<\/td><td>Delivery report by dates<\/td><td>POST <code>VoiceApi\/report\/dlr<\/code><\/td><td><code>fromdate<\/code>, <code>todate<\/code><\/td><td><code>response.reports_dlrList[]<\/code><\/td><\/tr><tr><td>10<\/td><td>Delivery report by transaction<\/td><td>GET <code>VoiceApi\/report\/dlr?tId=true<\/code><\/td><td><code>uuid<\/code><\/td><td><code>response.reports_dlrList[]<\/code><\/td><\/tr><tr><td>11<\/td><td>Delivery report by mobile<\/td><td>GET <code>VoiceApi\/report\/dlr?isMob=true<\/code><\/td><td><code>mobile<\/code><\/td><td><code>response.reports_dlrList[]<\/code><\/td><\/tr><tr><td>12<\/td><td>Daily summary<\/td><td>POST <code>VoiceApi\/report\/summary<\/code><\/td><td><code>fromdate<\/code>, <code>todate<\/code><\/td><td><code>response.report_summaryList[]<\/code><\/td><\/tr><tr><td>13<\/td><td>Bulk upload history<\/td><td>POST <code>VoiceApi\/report\/uploadhistory<\/code><\/td><td><code>fromdate<\/code>, <code>todate<\/code><\/td><td><code>response.reports_uploadHistoryList[]<\/code><\/td><\/tr><tr><td>14<\/td><td>Daily archive<\/td><td>GET <code>VoiceApi\/report\/day<\/code><\/td><td><code>date<\/code><\/td><td>a <code>.zip<\/code> file<\/td><\/tr><\/tbody><\/table><\/div><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Two shapes come back. Write operations (send, save, update, cancel) return a flat object with <code>status<\/code>, <code>statusCode<\/code> and <code>reason<\/code> at the top level. Read operations wrap everything in <code>response<\/code>, with <code>status<\/code>, <code>code<\/code>, <code>msg<\/code>, <code>api<\/code>, <code>action<\/code>, <code>cursors<\/code>, <code>pages<\/code> and <code>count<\/code> alongside the list.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Look closely at the list names. Delivery, schedule and upload lists are <code>reports_...List<\/code>, plural. The summary list is <code>report_summaryList<\/code>, singular. A generic reader that builds the key from a pattern will miss one of them, so map each endpoint to its list key explicitly.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The voice family also differs from the <code>SMSApi\/<\/code> families you may already have integrated. There, list rows are often wrapped in an extra object (<code>scheduleList[i].schedule<\/code>, for example). Here, each row is the object itself: <code>response.reports_dlrList[i].msgId<\/code>, not <code>response.reports_dlrList[i].dlr.msgId<\/code>. If you share a parser between products, branch on product, not on a guess.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"auth-encoding\" class=\"wp-block-heading\">Authentication and Request Encoding<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Every request needs <code>userid<\/code>. Then you either add <code>password<\/code> as a field, or send your API key in the <code>apikey<\/code> header. The reference defines the header scheme in so many words: if you use an API key, there&#8217;s no need to send the password. Use the header. It keeps the password out of query strings, server logs and browser history, and it lets you rotate the key without touching the login. (The <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/api-key-rotation-sms-api-without-dropping-messages\/\">API key rotation guide<\/a> covers doing that without dropping traffic.)<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>POST \/VoiceApi\/send HTTP\/1.1\nHost: unify.smsgateway.center\napikey: YOUR_API_KEY\nContent-Type: multipart\/form-data; boundary=----b\n\n------b\nContent-Disposition: form-data; name=\"userid\"\n\nYourUsername\n------b\nContent-Disposition: form-data; name=\"sendMethod\"\n\nquick\n------b--<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Header names are case-insensitive in HTTP (<a href=\"https:\/\/www.rfc-editor.org\/info\/rfc9110\" target=\"_blank\" rel=\"noopener nofollow\">RFC 9110, section 5.1<\/a>), so <code>apikey<\/code>, <code>apiKey<\/code> and <code>APIKEY<\/code> all reach the server as the same header.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The POST operations declare <code>multipart\/form-data<\/code> request bodies. That matters more than it looks. In a multipart form, every field is a string of bytes. The schema can say <code>libraryId<\/code> is an <code>integer<\/code> and <code>duplicateCheck<\/code> is a <code>boolean<\/code>, but on the wire you&#8217;re sending the characters <code>7432106150028594097<\/code> and <code>true<\/code>. The integer typing only becomes dangerous when a tool reads the schema and generates a client in a language with lossy numbers. We&#8217;ll get to that in <a href=\"#big-numbers\">Big Numbers<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The GET operations (voice OTP, clip renewal, the two DLR searches and the daily archive) take their parameters in the query string. For those, URL-encode every value, and keep <code>password<\/code> out of them entirely by using the header.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Every operation accepts <code>output<\/code> with <code>json<\/code>, <code>plain<\/code> or <code>xml<\/code>. Ask for <code>json<\/code> and parse JSON. The renewal and cancellation descriptions mention <code>format<\/code> instead of <code>output<\/code>; the parameter tables for both say <code>output<\/code>. Send <code>output=json<\/code>, and if you want belt and braces, send <code>format=json<\/code> too. An extra form field costs nothing.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"sending-calls\" class=\"wp-block-heading\">Sending a Call: quick, group and bulkupload<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><code>POST VoiceApi\/send<\/code> places one or more outbound calls that play a clip from your audio library. The request fields, as the reference defines them:<\/p>\n\n\n\n<figure><div class=\"table-responsive\"><table class=\"table table-striped table-bordered table-hover\"><thead><tr><th>Field<\/th><th>Type in the reference<\/th><th>Values and notes<\/th><\/tr><\/thead><tbody><tr><td><code>userid<\/code><\/td><td>string<\/td><td>Always required<\/td><\/tr><tr><td><code>password<\/code><\/td><td>string<\/td><td>Omit when you send the <code>apikey<\/code> header<\/td><\/tr><tr><td><code>audioType<\/code><\/td><td>string<\/td><td><code>library<\/code> for this operation<\/td><\/tr><tr><td><code>sendMethod<\/code><\/td><td>string enum<\/td><td><code>quick<\/code> needs <code>mobile<\/code>, <code>group<\/code> needs <code>group<\/code>, <code>bulkupload<\/code> needs <code>phoneList<\/code><\/td><\/tr><tr><td><code>libraryId<\/code><\/td><td>integer<\/td><td>The clip to play, a 19-digit value such as <code>7432106150028594097<\/code><\/td><\/tr><tr><td><code>mobile<\/code><\/td><td>string<\/td><td>Comma-separated numbers in international format, for <code>quick<\/code><\/td><\/tr><tr><td><code>group<\/code><\/td><td>string<\/td><td>Group IDs or names, for <code>group<\/code><\/td><\/tr><tr><td><code>phoneList<\/code><\/td><td>binary file<\/td><td>A CSV upload, for <code>bulkupload<\/code><\/td><\/tr><tr><td><code>identifier<\/code><\/td><td>string<\/td><td>Free text to label the campaign, such as <code>My Campaign<\/code><\/td><\/tr><tr><td><code>scheduleTime<\/code><\/td><td>string<\/td><td>Optional, <code>YYYY-MM-DD HH:MM<\/code><\/td><\/tr><tr><td><code>duplicateCheck<\/code><\/td><td>boolean<\/td><td><code>true<\/code> or <code>false<\/code><\/td><\/tr><tr><td><code>reDial<\/code><\/td><td>integer enum<\/td><td><code>0<\/code>, <code>1<\/code> or <code>2<\/code><\/td><\/tr><tr><td><code>redialInterval<\/code><\/td><td>integer enum<\/td><td><code>5<\/code>, <code>10<\/code> or <code>15<\/code> (minutes)<\/td><\/tr><tr><td><code>output<\/code><\/td><td>string enum<\/td><td><code>json<\/code>, <code>plain<\/code>, <code>xml<\/code><\/td><\/tr><\/tbody><\/table><\/div><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The success reply is flat:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"status\": \"success\",\n  \"mobile\": \"919xxxxxxxx1\",\n  \"invalidMobile\": \"\",\n  \"transactionId\": \"6305583318236810379\",\n  \"statusCode\": \"200\",\n  \"reason\": \"success\"\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">A few things to do with it straight away.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Save <code>transactionId<\/code> as text before you do anything else.<\/strong> It&#8217;s the only handle you get on this request. Everything later (schedule reads, moves, cancels, delivery lookups) needs it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Read <code>invalidMobile<\/code>.<\/strong> It&#8217;s a string, empty in the sample. When you send a comma list, some numbers can be accepted and some rejected in one call. Split <code>invalidMobile<\/code> on commas, trim, drop empties, and mark those recipients as rejected at submission in your own table. Don&#8217;t wait for a delivery report that will never come for them.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Treat <code>mobile<\/code> as an echo, not a receipt.<\/strong> It tells you what the platform accepted. Your per-recipient truth comes from the delivery rows, which carry their own <code>msgId<\/code> for each call.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Check <code>status<\/code>, not <code>statusCode<\/code>.<\/strong> <code>statusCode<\/code> is the string <code>\"200\"<\/code> here, but voice and SMS families quote and unquote their codes differently, and the error shape for voice isn&#8217;t spelled out in the reference. Read <code>status<\/code>, return on anything other than <code>success<\/code>, and only then bind the rest of the payload.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Label every campaign with <code>identifier<\/code>.<\/strong> It comes back on delivery rows as <code>identifier<\/code>, and on schedule rows the campaign label appears as <code>campaignName<\/code>. Put your own campaign or job ID in it (a short ASCII string), so you can join platform rows back to your jobs even when a transaction ID goes missing.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Which sendMethod to use<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Use <code>quick<\/code> for anything driven by an event in your system: one person, or a handful you already know. Use <code>group<\/code> when the audience already lives in a platform contact group you maintain through the group and contact APIs. Use <code>bulkupload<\/code> when you&#8217;re generating a large list offline and want the platform to ingest a file. Bulk uploads produce an upload history row (see <a href=\"#summary-archive\">the summary and archive section<\/a>) that tells you how many entries were valid, so read it before you assume the campaign went out.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">There&#8217;s a second, older <code>sendMethod<\/code> in the knowledge base. The <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/kb\/voice-call-api-bulk-obd-automation-smsgatewaycenter\/\">Voice Call API KB entry<\/a> shows <code>sendMethod=simpleVoice<\/code> with <code>audioType<\/code> set to <code>audio<\/code> (upload a file in the same call), <code>t2s<\/code> (text to speech, with <code>msg<\/code> and <code>msgType=text<\/code>) or <code>library<\/code>. The OpenAPI file only describes the library path. If you need text to speech or inline audio, test those calls in your account first and pin the exact field names your account accepts in a contract test; the <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/contract-testing-harness-messaging-api\/\">contract testing guide<\/a> shows how.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"voice-otp\" class=\"wp-block-heading\">Voice OTP: the PIN Send<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The voice OTP call reads a one-time code aloud. The OpenAPI file defines it as <code>GET VoiceApi\/send?sendMethod=pin<\/code> with these query parameters:<\/p>\n\n\n\n<figure><div class=\"table-responsive\"><table class=\"table table-striped table-bordered table-hover\"><thead><tr><th>Parameter<\/th><th>Type in the reference<\/th><th>Notes<\/th><\/tr><\/thead><tbody><tr><td><code>userid<\/code><\/td><td>string<\/td><td>Required<\/td><\/tr><tr><td><code>password<\/code><\/td><td>string<\/td><td>Omit with the <code>apikey<\/code> header<\/td><\/tr><tr><td><code>mobile<\/code><\/td><td>string<\/td><td>Recipient in international format<\/td><\/tr><tr><td><code>pin<\/code><\/td><td>integer<\/td><td>The OTP to read out, sample <code>123456<\/code><\/td><\/tr><tr><td><code>pinClipId<\/code><\/td><td>string<\/td><td>The clip played with the PIN<\/td><\/tr><tr><td><code>output<\/code><\/td><td>string enum<\/td><td><code>json<\/code>, <code>plain<\/code>, <code>xml<\/code><\/td><\/tr><\/tbody><\/table><\/div><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The KB pages for voice OTP add <code>audioType=pin<\/code>, <code>pinRepeatCount<\/code> (how many times the clip and PIN repeat, 1 or 2), <code>reDial<\/code>, <code>redialInterval<\/code>, <code>duplicateCheck<\/code>, and name the stock clips: <code>OneTimePassword<\/code>, <code>VerificationCode<\/code>, <code>Default<\/code>, <code>FollowingCode<\/code>, <code>Registration<\/code> and <code>UseThisCode<\/code> (see <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/kb\/what-parameters-are-required-to-send-a-voice-otp\/\">what parameters a voice OTP needs<\/a> and <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/kb\/how-does-the-voice-call-otp-pin-api-work\/\">how the PIN API works<\/a>). The reply has the same flat shape as a normal send, with a <code>transactionId<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The integer PIN trap<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>pin<\/code> is typed as an integer. OTPs are not integers. They&#8217;re short strings of digits, and a meaningful share of them start with zero. With six-digit codes drawn uniformly, one in ten starts with <code>0<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If your code holds the OTP in an integer variable, or a generated client serialises it from an integer field, <code>012345<\/code> becomes <code>12345<\/code>. The user hears five digits, types five digits, and your verifier compares against six. Your login funnel quietly loses about one attempt in ten, and nothing in your logs says why.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Fix it at both ends:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Generate OTPs as strings, and keep them as strings everywhere.<\/li>\n\n\n\n<li>Put the string straight into the query: <code>pin=012345<\/code>. URL encoding leaves digits alone.<\/li>\n\n\n\n<li>If you use a generated client, override the <code>pin<\/code> field to a string type, or bypass the client for this one call.<\/li>\n\n\n\n<li>Or generate codes without a leading zero (first digit from 1 to 9). That shrinks the code space by 10 percent, which is a fair trade if you can&#8217;t control every serialiser in the path.<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\">Voice OTP next to SMS OTP<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Voice OTP is usually a fallback: the SMS didn&#8217;t arrive, or the user tapped &#8220;call me instead&#8221;. Two timing rules keep it honest.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">First, the OTP has to still be valid when the call is answered. With platform redial enabled, the last attempt can start up to <code>reDial x redialInterval<\/code> minutes after the first (see <a href=\"#redial\">Redial<\/a>). With <code>reDial=2<\/code> and <code>redialInterval=15<\/code>, that&#8217;s 30 minutes. Most OTPs expire long before that. For voice OTP, set <code>reDial=0<\/code> and let the user ask again, or make sure your OTP lifetime covers the redial window.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Second, a voice fallback for the same login attempt should read the <em>same<\/em> code as the SMS, not a new one. Two live codes for one attempt confuses people and doubles your attack surface. The <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/otp-delivery-india-reliability-checklist\/\">OTP delivery checklist<\/a> covers the SMS side of this.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"audio-library\" class=\"wp-block-heading\">The Audio Library: Upload, IDs and Renewal<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Library calls play a clip you&#8217;ve uploaded. <code>POST VoiceApi\/save<\/code> takes three fields beyond auth:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>libraryName<\/code>: a label you choose, such as <code>Test<\/code><\/li>\n\n\n\n<li><code>audioTrack<\/code>: the mp3 file, sent as a multipart file part<\/li>\n\n\n\n<li><code>output<\/code><\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The reply:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"status\": \"success\",\n  \"length\": \"15\",\n  \"frequency\": \"41000\",\n  \"libraryId\": \"6305583318236810379\",\n  \"statusCode\": \"200\",\n  \"reason\": \"success\"\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>libraryId<\/code> comes back as a quoted string. Then the send and renew requests define <code>libraryId<\/code> as an integer. Same value, two types. Store the string you received and send exactly that string back. Never round-trip it through a number.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>length<\/code> and <code>frequency<\/code> are quoted strings too, with no units attached. Save them raw next to the clip, and if you want a duration in seconds for your own UI, measure the file you uploaded with your own audio tooling rather than reading meaning into <code>length<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Renewal, and why you need your own clip registry<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>GET VoiceApi\/save?reUpload=true<\/code> with <code>libraryId<\/code> renews a clip &#8220;if audio track is expired&#8221;. So clips expire. There&#8217;s no published clip lifetime, and no endpoint to list your clips or read a clip&#8217;s status.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That leaves you one practical design: keep a clip registry in your own database.<\/p>\n\n\n\n<figure><div class=\"table-responsive\"><table class=\"table table-striped table-bordered table-hover\"><thead><tr><th>Column<\/th><th>Why<\/th><\/tr><\/thead><tbody><tr><td><code>library_id<\/code> (text)<\/td><td>The string from the upload reply<\/td><\/tr><tr><td><code>library_name<\/code><\/td><td>What you sent as <code>libraryName<\/code><\/td><\/tr><tr><td><code>source_sha256<\/code><\/td><td>Hash of the mp3 you uploaded, so you know which file a clip is<\/td><\/tr><tr><td><code>source_path<\/code><\/td><td>Where you keep the original; never delete it while the clip is in use<\/td><\/tr><tr><td><code>uploaded_at<\/code>, <code>renewed_at<\/code><\/td><td>Your own timestamps<\/td><\/tr><tr><td><code>length_raw<\/code>, <code>frequency_raw<\/code><\/td><td>The strings from the reply<\/td><\/tr><tr><td><code>last_ok_send_at<\/code><\/td><td>Updated whenever a send with this clip succeeds<\/td><\/tr><\/tbody><\/table><\/div><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Then add a pre-flight step before every scheduled campaign: renew the clip, check <code>status<\/code> is <code>success<\/code>, and only then submit. Renewing a clip that hasn&#8217;t expired yet should be harmless, but nothing confirms it, so try it once on a scratch clip in your account and watch what happens to the <code>libraryId<\/code> and your balance. If the ID changes on renewal, update the registry from the reply every time.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/how-to-send-voice-call-from-library\/\">voice library walkthrough<\/a> shows the same library in the dashboard, which is handy when you need to compare what your registry says with what the account holds.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"four-identifiers\" class=\"wp-block-heading\">One Call, Four Identifiers<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Follow a single scheduled library call through the API and you&#8217;ll meet its identifier under four names:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong><code>transactionId<\/code><\/strong> in the send reply, a quoted string: <code>\"6305583318236810379\"<\/code>.<\/li>\n\n\n\n<li><strong><code>uuid<\/code><\/strong> in the request to <code>report\/schedule<\/code>, <code>report\/schedulelog<\/code>, <code>updateSchedule<\/code>, <code>cancelSchedule<\/code> and <code>report\/dlr?tId=true<\/code>, typed as an integer.<\/li>\n\n\n\n<li><strong><code>uuId<\/code><\/strong> (capital I) in schedule rows and delivery rows, typed as a string.<\/li>\n\n\n\n<li><strong><code>msgId<\/code><\/strong> in each delivery row, typed as an integer. This one is different: it identifies one call to one number, while the other three identify the whole request.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">The reply from <code>updateSchedule<\/code> and <code>cancelSchedule<\/code> goes back to calling it <code>transactionId<\/code>. Their descriptions also say <code>transactionId<\/code> for the request, while their parameter tables say <code>uuid<\/code>. When the table and the prose disagree, send what the table says (<code>uuid<\/code>), because that&#8217;s what the Swagger &#8220;Try it out&#8221; form sends.<\/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-voice-call-lifecycle.svg\"><img decoding=\"async\" src=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/10\/diagram-voice-call-lifecycle.svg\" alt=\"Three-panel flow of a voice call through the API: send returns transactionId, schedule operations take uuid, delivery reports return uuId and a per-call msgId\" class=\"wp-image-3122\"\/><\/a><figcaption class=\"wp-element-caption\">One request, four names. Normalise them to one text column the moment each reply arrives.<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The fix is boring and it works: pick one internal name, say <code>provider_txn_id<\/code>, and map all three request-level spellings onto it at the edge of your code. Keep <code>msgId<\/code> separately as <code>provider_call_id<\/code>. Both are text columns.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Here&#8217;s a small adapter that does the mapping for every reply shape in this family:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>TXN_KEYS = (\"transactionId\", \"uuId\", \"uuid\")\n\ndef provider_txn_id(obj: dict) -&gt; str | None:\n    \"\"\"Return the request-level ID from any voice reply or row, as text.\"\"\"\n    for key in TXN_KEYS:\n        value = obj.get(key)\n        if value not in (None, \"\"):\n            return str(value)\n    return None\n\ndef provider_call_id(row: dict) -&gt; str | None:\n    value = row.get(\"msgId\")\n    return None if value in (None, \"\") else str(value)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>str(value)<\/code> is only safe if <code>value<\/code> was never a float. In Python, the standard <code>json<\/code> module parses big integers as exact <code>int<\/code>, so <code>str()<\/code> is fine. In other languages it isn&#8217;t, which is the next section.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"big-numbers\" class=\"wp-block-heading\">Big Numbers and the Types That Break Them<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Every identifier in the voice API is 18 or 19 digits. A 64-bit double, the only number type in JavaScript and the default for many JSON parsers, holds integers exactly only up to 2^53, which is 9,007,199,254,740,992 (16 digits). Above that, it rounds to the nearest value it can represent. <a href=\"https:\/\/www.rfc-editor.org\/info\/rfc8259\/\" target=\"_blank\" rel=\"noopener nofollow\">RFC 8259, section 6<\/a> warns about exactly this range for interoperable JSON numbers, and the <a href=\"https:\/\/en.wikipedia.org\/wiki\/IEEE_754\" target=\"_blank\" rel=\"noopener nofollow\">IEEE 754<\/a> format is why.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Here&#8217;s what happens to the sample values from the reference when they pass through a double, computed in Python with <code>int(float(v))<\/code>:<\/p>\n\n\n\n<figure><div class=\"table-responsive\"><table class=\"table table-striped table-bordered table-hover\"><thead><tr><th>Field<\/th><th>Sample value<\/th><th>After a double<\/th><\/tr><\/thead><tbody><tr><td><code>transactionId<\/code><\/td><td>6305583318236810379<\/td><td>6305583318236810240<\/td><\/tr><tr><td><code>libraryId<\/code><\/td><td>7432106150028594097<\/td><td>7432106150028594176<\/td><\/tr><tr><td><code>msgId<\/code><\/td><td>8358730171396737017<\/td><td>8358730171396737024<\/td><\/tr><tr><td><code>uuId<\/code> (DLR row)<\/td><td>759513504945227190<\/td><td>759513504945227136<\/td><\/tr><tr><td><code>uuid<\/code> (schedule sample)<\/td><td>3076070760769274250<\/td><td>3076070760769274368<\/td><\/tr><\/tbody><\/table><\/div><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Every one changes. And <code>JSON.parse<\/code> in Node prints <code>7432106150028594097<\/code> as <code>7432106150028594000<\/code>, so the corruption even looks plausible in a log.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The schedule row sample in the reference shows <code>uuId<\/code> as <code>\"6653312612485645000\"<\/code>. Those three trailing zeros are the pattern a 19-digit value shows after it&#8217;s been through a JavaScript number. Whatever produced that sample, it&#8217;s a good reminder of how quietly this happens.<\/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-voice-id-types-table.svg\"><img decoding=\"async\" src=\"https:\/\/www.smsgatewaycenter.com\/blog\/wp-content\/uploads\/2026\/10\/diagram-voice-id-types-table.svg\" alt=\"Table of six voice API fields showing where each appears, its type in the reference, a sample value, whether it survives a double, and the safe storage type\" class=\"wp-image-3121\"\/><\/a><figcaption class=\"wp-element-caption\">Six fields, one rule: text in your code, text in your database.<\/figcaption><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">Where each field can bite<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong><code>transactionId<\/code><\/strong> is a quoted string in the send reply. Safe on arrival. The risk is later, if you put it in a numeric column or a JSON body as a bare number.<\/li>\n\n\n\n<li><strong><code>uuid<\/code><\/strong> is typed integer in requests. In a multipart form it&#8217;s text anyway. In a generated TypeScript client it becomes <code>number<\/code>, and every schedule call you make with it targets the wrong ID.<\/li>\n\n\n\n<li><strong><code>uuId<\/code><\/strong> is typed string in report rows. Safe, as long as the server sends it quoted.<\/li>\n\n\n\n<li><strong><code>msgId<\/code><\/strong> is typed integer in delivery rows. If the server sends it as a bare number, <code>JSON.parse<\/code> in Node rounds it before your code ever sees it. Use a lossless parser.<\/li>\n\n\n\n<li><strong><code>libraryId<\/code><\/strong> is a string in the upload reply and an integer in send and renew requests. Keep the string.<\/li>\n\n\n\n<li><strong><code>mobileNo<\/code><\/strong> is typed integer in delivery rows, with a sample of <code>\"919xxxxxxxx1\"<\/code>, which isn&#8217;t a number at all. A twelve-digit Indian mobile number fits in a double, but it overflows a 32-bit int, and phone numbers are identifiers, not quantities. Text.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">The column types<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>provider_txn_id   TEXT  NOT NULL,   -- transactionId \/ uuid \/ uuId\nprovider_call_id  TEXT,             -- msgId, one per call\nlibrary_id        TEXT,\nmobile_e164       TEXT  NOT NULL<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Don&#8217;t use <code>BIGINT<\/code> &#8220;because it fits&#8221;. Java&#8217;s <code>long<\/code> and Postgres <code>BIGINT<\/code> do hold 19-digit values up to 9,223,372,036,854,775,807, but the first time one of those IDs passes through a dashboard, a spreadsheet export or a JavaScript service, it gets rounded. Text survives all of them.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"redial\" class=\"wp-block-heading\">Redial: Who Owns the Retry<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The platform can redial an unanswered call for you. <code>reDial<\/code> takes <code>0<\/code>, <code>1<\/code> or <code>2<\/code>, and <code>redialInterval<\/code> takes <code>5<\/code>, <code>10<\/code> or <code>15<\/code> minutes. So the platform will make at most three attempts, and if each wait is the interval, the last attempt starts at most <code>reDial x redialInterval<\/code> minutes after the first:<\/p>\n\n\n\n<figure><div class=\"table-responsive\"><table class=\"table table-striped table-bordered table-hover\"><thead><tr><th><code>reDial<\/code><\/th><th><code>redialInterval<\/code><\/th><th>Attempts<\/th><th>Last attempt starts after<\/th><\/tr><\/thead><tbody><tr><td>0<\/td><td>any<\/td><td>1<\/td><td>0 minutes<\/td><\/tr><tr><td>1<\/td><td>5<\/td><td>2<\/td><td>5 minutes<\/td><\/tr><tr><td>1<\/td><td>15<\/td><td>2<\/td><td>15 minutes<\/td><\/tr><tr><td>2<\/td><td>5<\/td><td>3<\/td><td>10 minutes<\/td><\/tr><tr><td>2<\/td><td>10<\/td><td>3<\/td><td>20 minutes<\/td><\/tr><tr><td>2<\/td><td>15<\/td><td>3<\/td><td>30 minutes<\/td><\/tr><\/tbody><\/table><\/div><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Now add your own retry logic. A worker that resubmits when it doesn&#8217;t see &#8220;answered&#8221; within ten minutes, on top of <code>reDial=2<\/code>, can put six calls on one person&#8217;s phone. For a reminder, that&#8217;s annoying. For a collections or healthcare line, it&#8217;s a complaint.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Pick one owner per call:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Let the platform own it<\/strong> for campaign calls. Set <code>reDial<\/code> and <code>redialInterval<\/code>, and make your own worker never resubmit a <code>transactionId<\/code> that the platform accepted. Your job is to read the result, not to call again.<\/li>\n\n\n\n<li><strong>Own it yourself<\/strong> for anything with business logic between attempts: an OTP that should switch channel, a call that should stop if the customer paid in the meantime. Send with <code>reDial=0<\/code>, and make the next attempt a new, deliberate send with its own idempotency key.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Never mix them for the same message. If you do want your own retry after the platform gives up, wait until the window in the table above has passed and the delivery row is final, then decide.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>duplicateCheck=true<\/code> asks the platform to drop duplicates, but the window and the matching rule (same number? same clip? same day?) aren&#8217;t spelled out. Keep your own idempotency key per logical call, as the <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/message-idempotency-preventing-duplicate-sends\/\">idempotency guide<\/a> describes, and treat <code>duplicateCheck<\/code> as a second net, not the first.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"scheduling\" class=\"wp-block-heading\">Scheduling, Moving and Cancelling Calls<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Add <code>scheduleTime<\/code> to a send and the call is booked for later. The send reply gives you the <code>transactionId<\/code> as usual. From there, four operations manage the booking.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Read the booking.<\/strong> <code>POST VoiceApi\/report\/schedule<\/code> with <code>uuid<\/code> returns:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"response\": {\n    \"msg\": \"success\",\n    \"cursors\": { \"next\": \"a\", \"start\": \"a\" },\n    \"reports_scheduleList\": &#91;\n      {\n        \"scheduledTimestamp\": \"1722487200000\",\n        \"total\": \"1\",\n        \"amount\": 0.12,\n        \"uuId\": \"6653312612485645000\",\n        \"campaignName\": \"\",\n        \"cursorId\": 10,\n        \"status\": \"PENDING\"\n      }\n    ],\n    \"code\": \"200\",\n    \"pages\": { \"current\": 1, \"last\": 1 },\n    \"count\": { \"total\": 1, \"current\": 1 },\n    \"action\": \"schedule\",\n    \"api\": \"reports\",\n    \"status\": \"success\"\n  }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>scheduledTimestamp<\/code> is epoch milliseconds as a quoted string. <code>total<\/code> is a quoted count of recipients. <code>amount<\/code> is an unquoted float. The <code>status<\/code> inside the row (<code>PENDING<\/code>) is the booking state; the <code>status<\/code> at the end of <code>response<\/code> is the API call&#8217;s result. Read the outer one first.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Read the calls inside it.<\/strong> <code>POST VoiceApi\/report\/schedulelog<\/code> takes the same <code>uuid<\/code> and is described as returning the individual messages in the booking. Its sample reply is identical to the <code>report\/schedule<\/code> sample, down to <code>action: \"schedule\"<\/code>. Parse it loosely, and log the first real reply you get so you can see which fields actually come back.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Move it.<\/strong> <code>POST VoiceApi\/updateSchedule<\/code> with <code>uuid<\/code> and a new <code>scheduleTime<\/code> (sample <code>2024-01-31 10:00:00<\/code>). The reply is flat: <code>{status, transactionId, statusCode, reason}<\/code>. Notice the <code>transactionId<\/code> in that sample (<code>4740098350240514988<\/code>) is a different number from the <code>uuid<\/code> in the request sample (<code>1658333450312107482<\/code>). Whether a move keeps the original ID or issues a new one, you can&#8217;t tell from the samples alone. Handle both: if the reply&#8217;s <code>transactionId<\/code> differs from what you sent, record it as the booking&#8217;s current ID and keep the old one as an alias.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Cancel it.<\/strong> <code>POST VoiceApi\/cancelSchedule<\/code> with <code>uuid<\/code>. Same flat reply.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">After a move or a cancel, read the booking back with <code>report\/schedule<\/code> and check <code>scheduledTimestamp<\/code> or <code>status<\/code> changed the way you asked. A <code>success<\/code> reply tells you the request was accepted. The read-back tells you what the platform now holds.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">A cutoff keeps cancels safe<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">What happens if you cancel a booking that has already started dialling? Some numbers may have been called and some not. Rather than depend on the answer, set a cutoff: your system refuses to move or cancel a booking less than, say, ten minutes before <code>scheduledTimestamp<\/code>. Inside the cutoff, the booking is final, and any change becomes a new send. The <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/scheduling-messages-sms-whatsapp-rcs-telegram\/\">scheduling guide<\/a> uses the same pattern for SMS bookings.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"delivery-reports\" class=\"wp-block-heading\">Reading Delivery Reports<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">There&#8217;s no push for voice delivery in the reference, so you poll. <code>VoiceApi\/report\/dlr<\/code> has three variants:<\/p>\n\n\n\n<figure><div class=\"table-responsive\"><table class=\"table table-striped table-bordered table-hover\"><thead><tr><th>Variant<\/th><th>Method<\/th><th>Filter<\/th><th>Use it for<\/th><\/tr><\/thead><tbody><tr><td>By dates<\/td><td>POST<\/td><td><code>fromdate<\/code>, <code>todate<\/code><\/td><td>Your regular sync job<\/td><\/tr><tr><td>By transaction<\/td><td>GET with <code>?tId=true<\/code><\/td><td><code>uuid<\/code><\/td><td>Checking one request right after it&#8217;s due<\/td><\/tr><tr><td>By mobile<\/td><td>GET with <code>?isMob=true<\/code><\/td><td><code>mobile<\/code><\/td><td>Support lookups (&#8220;did we call this person?&#8221;)<\/td><\/tr><\/tbody><\/table><\/div><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">All three return the same shape. Here it is assembled from the example values in the reference:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"response\": {\n    \"msg\": \"success\",\n    \"cursors\": { \"next\": \"0\", \"start\": \"0\" },\n    \"code\": \"200\",\n    \"pages\": { \"current\": 1, \"last\": 0 },\n    \"count\": { \"total\": 0, \"current\": 1 },\n    \"reports_dlrList\": &#91;\n      {\n        \"identifier\": \"\",\n        \"amount\": 0.2,\n        \"deliveryTime\": 0,\n        \"answeredDuration\": 0,\n        \"audioType\": \"pin\",\n        \"channel\": \"API\",\n        \"msgId\": 8358730171396737017,\n        \"cause\": \"Waiting For Report\",\n        \"pickUp\": 0,\n        \"audioLength\": 26,\n        \"mobileNo\": \"919xxxxxxxx1\",\n        \"uuId\": \"759513504945227190\",\n        \"globalErrorCode\": 108,\n        \"cursorId\": 0,\n        \"hangUp\": 0,\n        \"submitTime\": 1720330311336,\n        \"pulse\": 1,\n        \"status\": \"SUBMITTED\"\n      }\n    ],\n    \"action\": \"dlr\",\n    \"api\": \"reports\",\n    \"status\": \"success\"\n  }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>msgId<\/code> is shown unquoted because the schema types it as an integer. Plan for the server to send it that way, and parse accordingly.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The sample&#8217;s <code>pages.last<\/code> is <code>0<\/code> and <code>count.total<\/code> is <code>0<\/code> while the list holds one row. Don&#8217;t use <code>count.total<\/code> to decide whether the list is empty. Use the list&#8217;s length.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>?tId=true<\/code> and <code>?isMob=true<\/code> suffixes are part of the path as the reference defines it. Send them. They&#8217;re how the server tells the three searches apart on a single path, and sending a flag the server ignores costs nothing.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The descriptions for the by-transaction and by-mobile searches say &#8220;prepare a POST request&#8221;, while the definitions are GET. Use GET, which is what the definition says and what the Swagger form sends, and keep the method in one constant so you can flip it if your account answers differently.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">A sync job that doesn&#8217;t miss late rows<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Voice rows change after they&#8217;re written. A row starts at <code>SUBMITTED<\/code> with <code>cause<\/code> &#8220;Waiting For Report&#8221; and gets updated once the call ends. So a sync job has to read the same window more than once.<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Every few minutes, read the window from 2 hours ago to now.<\/li>\n\n\n\n<li>Upsert each row keyed on <code>provider_call_id<\/code> (the <code>msgId<\/code> as text).<\/li>\n\n\n\n<li>Never overwrite a final status with a non-final one (see the next section for &#8220;final&#8221;).<\/li>\n\n\n\n<li>Once a day, re-read the previous day in full, to catch anything the short window missed.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Upserts make re-reading free: the same row arriving twice just updates nothing. The <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/delivery-report-ingestion-system-of-record\/\">delivery report ingestion guide<\/a> builds the same pattern for SMS in more depth.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"dlr-fields\" class=\"wp-block-heading\">What a Voice DLR Row Tells You<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Voice rows carry more than SMS rows, because a call has a life of its own after it&#8217;s delivered. Here&#8217;s every field, with what you can safely do with it.<\/p>\n\n\n\n<figure><div class=\"table-responsive\"><table class=\"table table-striped table-bordered table-hover\"><thead><tr><th>Field<\/th><th>Sample<\/th><th>Type<\/th><th>What to do with it<\/th><\/tr><\/thead><tbody><tr><td><code>msgId<\/code><\/td><td>8358730171396737017<\/td><td>integer<\/td><td>Primary key for one call. Text.<\/td><\/tr><tr><td><code>uuId<\/code><\/td><td>&#8220;759513504945227190&#8221;<\/td><td>string<\/td><td>Joins the call to its request. Text.<\/td><\/tr><tr><td><code>mobileNo<\/code><\/td><td>&#8220;919xxxxxxxx1&#8221;<\/td><td>integer (sample is a string)<\/td><td>Recipient. Text.<\/td><\/tr><tr><td><code>identifier<\/code><\/td><td>&#8220;&#8221;<\/td><td>string<\/td><td>Your campaign label from the send.<\/td><\/tr><tr><td><code>status<\/code><\/td><td>&#8220;SUBMITTED&#8221;<\/td><td>string<\/td><td>Store raw; map with an allowlist.<\/td><\/tr><tr><td><code>cause<\/code><\/td><td>&#8220;Waiting For Report&#8221;<\/td><td>string<\/td><td>Human-readable reason. Store, display, never parse.<\/td><\/tr><tr><td><code>globalErrorCode<\/code><\/td><td>108<\/td><td>integer<\/td><td>Machine-readable reason. Store; map with an allowlist.<\/td><\/tr><tr><td><code>submitTime<\/code><\/td><td>1720330311336<\/td><td>integer<\/td><td>Epoch ms; the sample is 2024-07-07 05:31:51 UTC.<\/td><\/tr><tr><td><code>deliveryTime<\/code><\/td><td>0<\/td><td>integer<\/td><td>Store raw; 0 means nothing recorded yet.<\/td><\/tr><tr><td><code>pickUp<\/code><\/td><td>0<\/td><td>integer<\/td><td>Store raw; 0 means nothing recorded yet.<\/td><\/tr><tr><td><code>hangUp<\/code><\/td><td>0<\/td><td>integer<\/td><td>Store raw; 0 means nothing recorded yet.<\/td><\/tr><tr><td><code>answeredDuration<\/code><\/td><td>0<\/td><td>integer<\/td><td>Store raw; 0 in a fresh row.<\/td><\/tr><tr><td><code>audioLength<\/code><\/td><td>26<\/td><td>integer<\/td><td>Length of the clip. Store raw.<\/td><\/tr><tr><td><code>pulse<\/code><\/td><td>1<\/td><td>integer<\/td><td>Store raw for billing reconciliation.<\/td><\/tr><tr><td><code>amount<\/code><\/td><td>0.2<\/td><td>float<\/td><td>Charge for the row.<\/td><\/tr><tr><td><code>audioType<\/code><\/td><td>&#8220;pin&#8221;<\/td><td>string<\/td><td><code>pin<\/code> for voice OTP, <code>library<\/code> for clips.<\/td><\/tr><tr><td><code>channel<\/code><\/td><td>&#8220;API&#8221;<\/td><td>string<\/td><td>Where the send came from.<\/td><\/tr><tr><td><code>cursorId<\/code><\/td><td>0<\/td><td>integer<\/td><td>The row&#8217;s position in the result.<\/td><\/tr><\/tbody><\/table><\/div><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Three rules make this table safe to build on.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Zero means &#8220;nothing yet&#8221;, not &#8220;midnight 1970&#8221;.<\/strong> <code>deliveryTime<\/code>, <code>pickUp<\/code> and <code>hangUp<\/code> are 0 in the sample row. Store them raw, add a nullable column for each that you fill only when the value is nonzero, and chart from those. Otherwise every chart you draw has a spike at 1 January 1970.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Classify status with an allowlist that fails open to &#8220;pending&#8221;.<\/strong> The only values in the reference are <code>SUBMITTED<\/code> for rows and <code>PENDING<\/code> for bookings. The daily summary hints at the rest: it counts <code>success<\/code>, <code>pending<\/code>, <code>notSent<\/code>, <code>failed<\/code>, <code>others<\/code> and <code>refund<\/code>. So build a mapping from the raw <code>status<\/code> strings you actually observe to your own states, and send anything you haven&#8217;t mapped yet to <code>pending<\/code>. An unknown value then means &#8220;keep polling&#8221;, never &#8220;delivered&#8221; or &#8220;failed&#8221;.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>FINAL = {}           # fill from statuses you have observed, e.g. {\"ANSWERED\": \"answered\"}\nNON_FINAL = {\"SUBMITTED\": \"pending\"}\n\ndef call_state(raw_status: str) -&gt; tuple&#91;str, bool]:\n    s = (raw_status or \"\").strip().upper()\n    if s in FINAL:\n        return FINAL&#91;s], True\n    return NON_FINAL.get(s, \"pending\"), False<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Derive &#8220;answered&#8221; from the call fields, not from the status string.<\/strong> In a fresh row, <code>pickUp<\/code> and <code>answeredDuration<\/code> are both 0. Make one test call that you answer and one that you let ring out, and compare the rows. Once you&#8217;ve seen those fields go nonzero on an answer, they give you &#8220;answered&#8221; whatever the status vocabulary turns out to be. Leave voicemail on for a third call to see whether a voicemail box counts as a pickup.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If you want &#8220;heard the whole message&#8221;, compare <code>answeredDuration<\/code> with <code>audioLength<\/code>. Before you rely on that ratio, make three test calls to a phone you control with a clip of known length: hang up immediately, hang up halfway, and listen to the end. Look at the three rows. That tells you the units and what &#8220;full&#8221; looks like in your account.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"paging\" class=\"wp-block-heading\">Paging Without a Page Parameter<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Every report reply has <code>cursors<\/code> (<code>next<\/code>, <code>start<\/code>), <code>pages<\/code> (<code>current<\/code>, <code>last<\/code>), <code>count<\/code> (<code>total<\/code>, <code>current<\/code>) and a <code>cursorId<\/code> on each row. None of the report request schemas has a field to send a cursor or a page number back.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">So don&#8217;t plan on paging forward. Plan on asking for windows small enough that one reply holds everything, and prove it on every call:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Request a window, say one hour.<\/li>\n\n\n\n<li>If <code>pages.last<\/code> is greater than 1, or <code>count.total<\/code> is greater than the rows you got, the window was too big.<\/li>\n\n\n\n<li>Split it in half and request both halves.<\/li>\n\n\n\n<li>Repeat until every window comes back complete.<\/li>\n<\/ol>\n\n\n\n<pre class=\"wp-block-code\"><code>from datetime import datetime, timedelta\n\ndef fetch_window(client, start: datetime, end: datetime, min_span=timedelta(minutes=1)):\n    rows, meta = client.dlr_by_dates(start, end)\n    total = int(meta.get(\"count\", {}).get(\"total\") or 0)\n    last = int(meta.get(\"pages\", {}).get(\"last\") or 0)\n    complete = last &lt;= 1 and total &lt;= len(rows)\n    if complete or (end - start) &lt;= min_span:\n        if not complete:\n            client.alert(\"dlr window still truncated\", start, end, total, len(rows))\n        return rows\n    mid = start + (end - start) \/ 2\n    return (fetch_window(client, start, mid, min_span)\n            + fetch_window(client, mid, end, min_span))<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This works whatever the server&#8217;s page size is, and it keeps working if a page parameter appears later. Simulate a one-hour window holding 450 rows against a server that returns 100 rows per reply, and the function makes 15 requests and returns all 450. The halves overlap at <code>mid<\/code>, and the upsert on <code>msgId<\/code> absorbs any row that lands in both.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The sample&#8217;s <code>count.total<\/code> of 0 next to a one-row list (above) is why the check uses <code>total &lt;= len(rows)<\/code> rather than equality: a reply with more rows than its own count is fine; a reply with fewer is truncated.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Summary, Upload History and the Daily Archive {#summary-archive}<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Daily summary<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>POST VoiceApi\/report\/summary<\/code> with <code>fromdate<\/code> and <code>todate<\/code> returns one row per day in <code>response.report_summaryList<\/code> (singular <code>report_<\/code>):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"date\": 1720290600000,\n  \"amount\": 0.6,\n  \"total\": 6,\n  \"success\": 0,\n  \"pending\": 3,\n  \"notSent\": 2,\n  \"failed\": 1,\n  \"others\": 0,\n  \"cursorId\": 0,\n  \"refund\": 0\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The buckets add up in the sample: 0 + 3 + 2 + 1 + 0 = 6. Check that sum on every row you ingest, and alert if it doesn&#8217;t hold. It&#8217;s the cheapest data quality test you&#8217;ll ever write.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>date<\/code> is 1720290600000, which is 2024-07-06 18:30 UTC, exactly midnight on 7 July in India Standard Time. So the summary&#8217;s days are IST days. If your own reporting runs in UTC, your daily totals and the summary&#8217;s won&#8217;t line up until you bucket in IST too.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Use the summary to reconcile. Count your own delivery rows per IST day and per bucket, and compare with the summary. A gap tells you your sync job missed a window.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Upload history<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>POST VoiceApi\/report\/uploadhistory<\/code> lists your <code>bulkupload<\/code> files:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"identifier\": \"668a72f0d346b.csv\",\n  \"filename\": \"668a72f0d346b.csv\",\n  \"comments\": \"Total 9 uploaded, valid entries of 0 found and 9 invalid entries found. Total Duplicate entries found are 0.\",\n  \"uuId\": \"6764150675538529832\",\n  \"cursorId\": 1567,\n  \"timestamp\": 1720349424941\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The valid and invalid counts live inside the free-text <code>comments<\/code>. Parse them with a forgiving pattern, keep the raw string, and alert if the pattern stops matching:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import re\n\nUPLOAD_RE = re.compile(\n    r\"Total\\s+(\\d+)\\s+uploaded.*?valid entries of\\s+(\\d+).*?(\\d+)\\s+invalid\"\n    r\".*?Duplicate entries found are\\s+(\\d+)\", re.I | re.S)\n\ndef parse_upload_comment(text: str) -&gt; dict | None:\n    m = UPLOAD_RE.search(text or \"\")\n    if not m:\n        return None   # keep the raw comment and raise an alert\n    total, valid, invalid, dupes = map(int, m.groups())\n    return {\"total\": total, \"valid\": valid, \"invalid\": invalid, \"duplicates\": dupes}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The sample is worth a second look: 9 uploaded, 0 valid. A file in the wrong format gets accepted, gets a <code>uuId<\/code>, and calls nobody. After every bulk upload, find its history row by <code>uuId<\/code> and stop the campaign in your own system if <code>valid<\/code> is 0 or far below the row count of the file you sent.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Daily archive<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>GET VoiceApi\/report\/day<\/code> with <code>date<\/code> (<code>YYYY-MM-DD<\/code>) returns a zip of that day&#8217;s delivery report; the sample reply is a message saying you&#8217;ll be prompted with a <code>.zip<\/code> file. In code, check the first two bytes of the body are <code>PK<\/code> (the zip signature) before you unzip. If they aren&#8217;t, you&#8217;ve got a JSON or text error, so log it and stop. Use the archive for backfills and audits; use <code>report\/dlr<\/code> for anything recent.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"time-zones\" class=\"wp-block-heading\">Time Zones and Date Formats<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Voice requests take dates in three shapes, and none carries a zone:<\/p>\n\n\n\n<figure><div class=\"table-responsive\"><table class=\"table table-striped table-bordered table-hover\"><thead><tr><th>Where<\/th><th>Format in the field definition<\/th><th>Format in the description<\/th><\/tr><\/thead><tbody><tr><td><code>scheduleTime<\/code> on send<\/td><td><code>YYYY-MM-DD HH:MM<\/code><\/td><td><code>YYYY-MM-DD HH:MM:SS<\/code> (spelled <code>scheduletime<\/code>)<\/td><\/tr><tr><td><code>scheduleTime<\/code> on <code>updateSchedule<\/code><\/td><td>sample <code>2024-01-31 10:00:00<\/code><\/td><td><code>YYYY-MM-DD HH:MM:SS<\/code><\/td><\/tr><tr><td><code>fromdate<\/code>, <code>todate<\/code> on <code>report\/dlr<\/code><\/td><td><code>YYYY-MM-DD HH:MM<\/code><\/td><td><code>YYYY-MM-DD HH:MM:SS<\/code><\/td><\/tr><tr><td><code>fromdate<\/code>, <code>todate<\/code> on summary and upload history<\/td><td><code>YYYY-MM-DD HH:MM:SS<\/code><\/td><td><code>YYYY-MM-DD HH:MM:SS<\/code><\/td><\/tr><tr><td><code>date<\/code> on <code>report\/day<\/code><\/td><td><code>YYYY-MM-DD<\/code><\/td><td><code>YYYY-MM-DD<\/code><\/td><\/tr><\/tbody><\/table><\/div><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Replies, on the other hand, give epoch milliseconds, which have no zone problem at all. That asymmetry gives you a clean way to pin the input zone once:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Book a test call (to your own number) for a wall-clock time well in the future, such as <code>2030-01-01 10:00<\/code>.<\/li>\n\n\n\n<li>Read it back with <code>report\/schedule<\/code>.<\/li>\n\n\n\n<li>Convert <code>scheduledTimestamp<\/code> to UTC. If it&#8217;s 04:30 UTC, the platform read your input as IST. If it&#8217;s 10:00 UTC, it read UTC.<\/li>\n\n\n\n<li>Cancel the booking, and store the result as configuration.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Run the probe in CI or at deploy time, and refuse to schedule anything if the answer ever changes. A scheduler that silently shifts every call by five and a half hours is worse than one that stops.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The daily summary&#8217;s IST midnight (above) suggests IST, which matches the account default for an Indian platform. Measure it anyway. The probe takes a minute and replaces a guess with a fact.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For the format, send what the field definition asks for. If the platform rejects it, try the description&#8217;s format, and pin whichever one the read-back confirms in a contract test.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"client-samples\" class=\"wp-block-heading\">Four Client Samples, Four Traps<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Each sample below is short and does one thing, and each one shows a different way voice integrations lose data. Replace <code>YourUsername<\/code> and the key with your own, and point the first runs at your own phone number.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Python: send a voice OTP without losing the leading zero<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>import os\nimport requests\n\nBASE = \"https:\/\/unify.smsgateway.center\"\nHEADERS = {\"apikey\": os.environ&#91;\"SGC_API_KEY\"]}\nUSERID = os.environ&#91;\"SGC_USERID\"]\n\ndef send_voice_otp(mobile: str, pin: str, clip: str = \"OneTimePassword\") -&gt; str:\n    if not (pin.isdigit() and 4 &lt;= len(pin) &lt;= 8):\n        raise ValueError(\"pin must be a string of 4 to 8 digits\")\n    params = {\n        \"userid\": USERID,\n        \"sendMethod\": \"pin\",\n        \"audioType\": \"pin\",\n        \"mobile\": mobile,\n        \"pin\": pin,              # a string: \"012345\" stays six digits\n        \"pinClipId\": clip,\n        \"pinRepeatCount\": \"2\",\n        \"reDial\": \"0\",           # the OTP owns its own retry\n        \"output\": \"json\",\n    }\n    r = requests.get(f\"{BASE}\/VoiceApi\/send\", params=params, headers=HEADERS, timeout=15)\n    body = r.json()\n    if body.get(\"status\") != \"success\":\n        raise RuntimeError(f\"voice OTP rejected: {body!r}\")\n    return str(body&#91;\"transactionId\"])<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The trap is the type of <code>pin<\/code>. The validation at the top refuses anything but a digit string, so an integer can&#8217;t sneak in from a caller. <code>requests<\/code> sends <code>sendMethod=pin<\/code> in the query along with everything else, which matches the path in the reference. Note the timeout: a voice send that hangs should fail fast so your OTP flow can offer another channel.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Node.js: parse delivery rows without rounding msgId<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Node 18+ (global fetch and FormData)\nconst BASE = \"https:\/\/unify.smsgateway.center\";\n\n\/\/ Quote bare 15+ digit numbers for the keys we treat as identifiers,\n\/\/ before JSON.parse can round them.\nconst ID_KEYS = &#91;\"msgId\", \"mobileNo\", \"uuId\", \"uuid\", \"transactionId\", \"libraryId\"];\nconst BARE_ID = new RegExp(`\"(${ID_KEYS.join(\"|\")})\"\\\\s*:\\\\s*(-?\\\\d{15,})`, \"g\");\n\nfunction parseLossless(text) {\n  return JSON.parse(text.replace(BARE_ID, '\"$1\":\"$2\"'));\n}\n\nasync function dlrByDates(fromdate, todate) {\n  const form = new FormData();\n  form.set(\"userid\", process.env.SGC_USERID);\n  form.set(\"fromdate\", fromdate);   \/\/ \"2026-10-26 10:00\"\n  form.set(\"todate\", todate);\n  form.set(\"output\", \"json\");\n  const res = await fetch(`${BASE}\/VoiceApi\/report\/dlr`, {\n    method: \"POST\",\n    headers: { apikey: process.env.SGC_API_KEY },\n    body: form,\n  });\n  const body = parseLossless(await res.text());\n  const r = body.response || {};\n  if (r.status !== \"success\") throw new Error(`dlr read failed: ${JSON.stringify(r)}`);\n  return Array.isArray(r.reports_dlrList) ? r.reports_dlrList : &#91;];\n}\n\nmodule.exports = { parseLossless, dlrByDates };<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The trap is <code>JSON.parse<\/code>. Fed <code>{\"msgId\":8358730171396737017}<\/code>, it hands you <code>8358730171396737000<\/code>, and every upsert keyed on that value collides with the wrong row or misses the right one. The regex runs on the raw text first and turns those bare numbers into strings. It only touches the named keys and only numbers of 15 digits or more, so <code>amount<\/code>, <code>pulse<\/code> and timestamps stay numbers. If you&#8217;d rather use a library, <code>json-bigint<\/code> or a reviver with source access does the same job.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Running the function above on <code>{\"msgId\":8358730171396737017,\"amount\":0.2}<\/code> gives <code>{ msgId: '8358730171396737017', amount: 0.2 }<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Java: keep phone numbers and PINs out of int<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>import java.net.URI;\nimport java.net.URLEncoder;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\npublic class VoiceOtp {\n    private static final Pattern TXN =\n        Pattern.compile(\"\\\"transactionId\\\"\\\\s*:\\\\s*\\\"?(\\\\d+)\\\"?\");\n\n    static String enc(String v) {\n        return URLEncoder.encode(v, StandardCharsets.UTF_8);\n    }\n\n    public static String send(String userid, String apiKey, String mobile, String pin)\n            throws Exception {\n        \/\/ mobile and pin are Strings on purpose:\n        \/\/ 919999999999 overflows int, and \"012345\" as an int is 12345.\n        String query = \"userid=\" + enc(userid)\n            + \"&amp;sendMethod=pin&amp;audioType=pin\"\n            + \"&amp;mobile=\" + enc(mobile)\n            + \"&amp;pin=\" + enc(pin)\n            + \"&amp;pinClipId=OneTimePassword&amp;reDial=0&amp;output=json\";\n        HttpRequest req = HttpRequest.newBuilder(\n                URI.create(\"https:\/\/unify.smsgateway.center\/VoiceApi\/send?\" + query))\n            .header(\"apikey\", apiKey)\n            .timeout(Duration.ofSeconds(15))\n            .GET()\n            .build();\n        String body = HttpClient.newHttpClient()\n            .send(req, HttpResponse.BodyHandlers.ofString()).body();\n        if (!body.contains(\"\\\"success\\\"\")) {\n            throw new IllegalStateException(\"voice OTP rejected: \" + body);\n        }\n        Matcher m = TXN.matcher(body);\n        if (!m.find()) throw new IllegalStateException(\"no transactionId: \" + body);\n        return m.group(1);   \/\/ keep it a String\n    }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The trap here is quieter. A Java <code>long<\/code> holds every voice ID, so Java developers tend to relax. But generated clients and DTOs often map &#8220;integer&#8221; to <code>Integer<\/code>, and a twelve-digit mobile number like 919999999999 is far past <code>Integer.MAX_VALUE<\/code> (2,147,483,647). Keep <code>mobile<\/code>, <code>pin<\/code> and every ID as <code>String<\/code> in your DTOs, whatever the schema says. The regex extraction keeps this sample on the JDK alone; in a real service, bind with your JSON library into <code>String<\/code> fields. For brevity, the <code>success<\/code> check is a substring test; in production, parse the body and read <code>status<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">cURL: upload a clip, and don&#8217;t trust jq with IDs<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code># Upload an mp3 to the audio library\ncurl -sS https:\/\/unify.smsgateway.center\/VoiceApi\/save \\\n  -H \"apikey: $SGC_API_KEY\" \\\n  -F \"userid=$SGC_USERID\" \\\n  -F \"libraryName=welcome-v3\" \\\n  -F \"audioTrack=@welcome-v3.mp3;type=audio\/mpeg\" \\\n  -F \"output=json\" \\\n  -o save-reply.json\n\n# Pull libraryId as raw text, not as a number\ngrep -o '\"libraryId\"&#91;&#91;:space:]]*:&#91;&#91;:space:]]*\"\\?&#91;0-9]*' save-reply.json \\\n  | grep -o '&#91;0-9]*$'<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Two traps. First, the <code>@<\/code> in <code>audioTrack=@welcome-v3.mp3<\/code> tells curl to attach the file. Leave it out and curl sends the literal text <code>welcome-v3.mp3<\/code> as the field value, and you get an error, or worse, a &#8220;clip&#8221; that isn&#8217;t one. Second, <code>jq<\/code> 1.6 parses numbers as doubles: <code>echo '{\"msgId\":8358730171396737017}' | jq .msgId<\/code> prints <code>8358730171396737000<\/code>. <code>libraryId<\/code> arrives quoted, so <code>jq -r .libraryId<\/code> is safe for that field, but <code>msgId<\/code> in delivery rows may not be. Check <code>jq --version<\/code>, test it on a known 19-digit value, and when in doubt pull IDs out with <code>grep -o<\/code> as above.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"data-model\" class=\"wp-block-heading\">A Data Model for Voice Calls<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Voice fits next to the outbound table you may already have for SMS (the <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/outbound-message-table-schema-design\/\">outbound message table guide<\/a> covers the general shape). The difference is that one voice request fans out into many calls, each with a life after delivery. Two tables cover it.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>CREATE TABLE voice_request (\n  id                BIGSERIAL PRIMARY KEY,\n  idempotency_key   TEXT NOT NULL UNIQUE,     -- yours, per logical send\n  provider_txn_id   TEXT UNIQUE,              -- transactionId \/ uuid \/ uuId\n  previous_txn_ids  TEXT&#91;] NOT NULL DEFAULT '{}', -- if a reschedule issues a new one\n  kind              TEXT NOT NULL CHECK (kind IN ('library','pin')),\n  send_method       TEXT NOT NULL,            -- quick | group | bulkupload | pin\n  library_id        TEXT,                     -- for library calls\n  pin_clip_id       TEXT,                     -- for voice OTP\n  identifier        TEXT,                     -- what you sent as identifier\n  redial            SMALLINT NOT NULL CHECK (redial BETWEEN 0 AND 2),\n  redial_interval   SMALLINT CHECK (redial_interval IN (5,10,15)),\n  scheduled_for     TIMESTAMPTZ,              -- your intent, in UTC\n  scheduled_epoch_ms TEXT,                    -- scheduledTimestamp as read back\n  invalid_mobiles   TEXT&#91;] NOT NULL DEFAULT '{}',\n  state             TEXT NOT NULL,            -- queued | submitted | booked | cancelled | rejected\n  created_at        TIMESTAMPTZ NOT NULL DEFAULT now()\n);\n\nCREATE TABLE voice_call (\n  provider_call_id  TEXT PRIMARY KEY,         -- msgId\n  provider_txn_id   TEXT NOT NULL,            -- uuId on the row\n  mobile_e164       TEXT NOT NULL,            -- mobileNo\n  status_raw        TEXT NOT NULL,\n  cause_raw         TEXT,\n  global_error_code TEXT,\n  state             TEXT NOT NULL,            -- from the allowlist; unknown -&gt; pending\n  is_final          BOOLEAN NOT NULL DEFAULT false,\n  submit_time_ms    TEXT,\n  delivery_time_raw TEXT,\n  pick_up_raw       TEXT,\n  hang_up_raw       TEXT,\n  answered_duration_raw TEXT,\n  audio_length_raw  TEXT,\n  pulse_raw         TEXT,\n  amount_raw        TEXT,                     -- keep the provider's exact value\n  audio_type        TEXT,\n  channel           TEXT,\n  first_seen_at     TIMESTAMPTZ NOT NULL DEFAULT now(),\n  last_seen_at      TIMESTAMPTZ NOT NULL DEFAULT now()\n);\n\nCREATE INDEX voice_call_txn ON voice_call (provider_txn_id);\nCREATE INDEX voice_call_mobile ON voice_call (mobile_e164);<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The upsert from a delivery row should only ever move a call forward:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>INSERT INTO voice_call AS c (provider_call_id, provider_txn_id, mobile_e164,\n       status_raw, cause_raw, global_error_code, state, is_final, last_seen_at)\nVALUES ($1, $2, $3, $4, $5, $6, $7, $8, now())\nON CONFLICT (provider_call_id) DO UPDATE\nSET status_raw = EXCLUDED.status_raw,\n    cause_raw = EXCLUDED.cause_raw,\n    global_error_code = EXCLUDED.global_error_code,\n    state = EXCLUDED.state,\n    is_final = EXCLUDED.is_final,\n    last_seen_at = now()\nWHERE NOT c.is_final;      -- a final row is never overwritten<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Everything raw goes in as text. You&#8217;ll want it the day someone asks why a call cost what it did, or why your &#8220;answered&#8221; count doesn&#8217;t match the dashboard. Derived columns (answered, heard to the end, cost in your currency) live in views on top, so you can fix a derivation without re-ingesting history.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If you keep call records about people, they fall under your retention and erasure rules too. The <a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/messaging-data-retention-erasure-sms-whatsapp-rcs-telegram\/\">data retention and erasure guide<\/a> shows how to redact phone numbers while keeping opt-outs working.<\/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\">Production Checklist<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Auth uses <code>userid<\/code> plus the <code>apikey<\/code> header; no passwords in query strings.<\/li>\n\n\n\n<li>Every identifier (<code>transactionId<\/code>, <code>uuid<\/code>, <code>uuId<\/code>, <code>msgId<\/code>, <code>libraryId<\/code>) is text in code, queues, logs and the database.<\/li>\n\n\n\n<li><code>mobile<\/code>, <code>mobileNo<\/code> and <code>pin<\/code> are strings everywhere, including generated DTOs.<\/li>\n\n\n\n<li>Node services parse voice replies with a lossless parser; shell scripts don&#8217;t use <code>jq<\/code> on IDs without a version check.<\/li>\n\n\n\n<li><code>transactionId<\/code> is saved before any other processing of a send reply.<\/li>\n\n\n\n<li><code>invalidMobile<\/code> is split and recorded as rejected at submission.<\/li>\n\n\n\n<li>Every send carries <code>identifier<\/code> set to your own campaign or job ID.<\/li>\n\n\n\n<li>Each call has exactly one retry owner: platform <code>reDial<\/code>, or your code with <code>reDial=0<\/code>.<\/li>\n\n\n\n<li>Voice OTP lifetime covers <code>reDial x redialInterval<\/code>, or voice OTP uses <code>reDial=0<\/code>.<\/li>\n\n\n\n<li>A clip registry holds <code>library_id<\/code>, source file hash and path; clips are renewed in a pre-flight before campaigns.<\/li>\n\n\n\n<li>The input time zone is pinned by a schedule-and-read-back probe at deploy time.<\/li>\n\n\n\n<li>Moves and cancels respect a cutoff before <code>scheduledTimestamp<\/code>, and every change is read back.<\/li>\n\n\n\n<li>The DLR sync re-reads a trailing window, upserts on <code>msgId<\/code>, and never overwrites a final row.<\/li>\n\n\n\n<li>Report windows are split until <code>pages.last<\/code> is at most 1 and <code>count.total<\/code> is at most the rows returned.<\/li>\n\n\n\n<li>Status mapping is an allowlist; unknown values map to pending.<\/li>\n\n\n\n<li>Zero timestamps are treated as &#8220;nothing yet&#8221;.<\/li>\n\n\n\n<li>Daily summary rows are checked for <code>success + pending + notSent + failed + others = total<\/code>, and reconciled against your own rows by IST day.<\/li>\n\n\n\n<li>Every bulk upload is followed by an upload history check on valid entries.<\/li>\n\n\n\n<li>Archive downloads are checked for the <code>PK<\/code> zip signature before unzipping.<\/li>\n\n\n\n<li>A contract test pins the reply shapes your account actually returns.<\/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\">Some behaviours of the voice API aren&#8217;t pinned down by anything you can read today. Each item below gives you the choice that stays safe whichever way the answer goes, and none of them needs you to wait for one.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>One. Send <code>pin<\/code> as a digit string and generate it as one.<\/strong> Whether the server reads <code>pin<\/code> as text or as a number, a string with its leading zero intact is correct in both cases, and an integer is wrong in one of them.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Two. Try the OTP <code>sendMethod<\/code> values in a sandbox and pin the one that works.<\/strong> The reference path uses <code>sendMethod=pin<\/code>, one KB page shows <code>quick<\/code>, another shows <code>quickce<\/code>, each with <code>audioType=pin<\/code>. Send a test OTP to your own phone with each, keep the one your account answers with <code>success<\/code> and an actual call, and lock it in a contract test so a change shows up as a failing test rather than silent OTP failures.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Three. Make one owner responsible for every retry.<\/strong> Whether <code>redialInterval<\/code> is measured from the start or the end of the previous attempt, a call owned by either the platform or your code (never both) can&#8217;t ring someone more times than you intended.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Four. Keep your own idempotency key, and treat <code>duplicateCheck<\/code> as a backstop.<\/strong> Whatever window and matching rule <code>duplicateCheck<\/code> uses, a unique key per logical call in your own database stops the duplicate before it leaves your system.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Five. Treat every report as unpageable and size your windows to fit.<\/strong> If a cursor or page parameter exists and isn&#8217;t listed, the window-splitting reader still returns every row. If it doesn&#8217;t exist, the window-splitting reader is the only thing that does.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Six. Map statuses through an allowlist where unknown means pending.<\/strong> The full set of row statuses isn&#8217;t published. An unknown value that maps to pending means more polling. An unknown value that maps to delivered or failed means a wrong answer you&#8217;ll never notice.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Seven. Measure the input time zone with a booking and a read-back.<\/strong> <code>scheduleTime<\/code>, <code>fromdate<\/code> and <code>todate<\/code> carry no zone, while <code>scheduledTimestamp<\/code> comes back as epoch milliseconds. One test booking turns the question into a number you can compare, and a deploy-time check catches it if the answer ever changes.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Eight. After a reschedule, record whatever <code>transactionId<\/code> comes back.<\/strong> Whether <code>updateSchedule<\/code> keeps the ID or issues a new one, storing the reply&#8217;s value as current and keeping the old one as an alias means every later lookup works.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Nine. Never cancel inside your cutoff.<\/strong> Nobody can tell you exactly what <code>cancelSchedule<\/code> does to a booking that has started dialling. A cutoff before <code>scheduledTimestamp<\/code> means you never have to find out.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Ten. Renew clips before campaigns and keep the source file.<\/strong> With no published clip lifetime and no endpoint to list clips, a pre-flight renewal plus your own registry means an expired clip shows up as a failed pre-flight, not as a campaign that played silence.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Eleven. Parse every voice reply losslessly, even fields the schema calls strings.<\/strong> If <code>msgId<\/code> arrives bare, a lossless parser keeps it exact. If it arrives quoted, the lossless parser leaves it alone. Either way the ID you store is the ID the platform sent.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Twelve. Validate test calls before trusting call-progress fields.<\/strong> Three calls to your own phone (rejected, answered briefly, answered to the end) tell you what <code>pickUp<\/code>, <code>hangUp<\/code>, <code>answeredDuration<\/code> and <code>audioLength<\/code> look like in practice, and protect every metric you build on them.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 id=\"faqs\" class=\"wp-block-heading\">FAQs<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">What is the base URL for the Voice Call API?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>https:\/\/unify.smsgateway.center<\/code>, with every operation under <code>\/VoiceApi\/<\/code>. Calls are sent with <code>VoiceApi\/send<\/code>, clips are uploaded with <code>VoiceApi\/save<\/code>, and reports live under <code>VoiceApi\/report\/<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Can I use an API key instead of a password?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Yes. Send <code>userid<\/code> as a field and the key in the <code>apikey<\/code> header, and leave <code>password<\/code> out. That keeps the password out of URLs and logs.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">How do I send a voice OTP?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Use a GET to <code>VoiceApi\/send?sendMethod=pin<\/code> with <code>userid<\/code>, <code>mobile<\/code>, <code>pin<\/code>, <code>pinClipId<\/code> and <code>output=json<\/code>. Send <code>pin<\/code> as a string so a code like <code>012345<\/code> keeps its leading zero, and set <code>reDial=0<\/code> unless your OTP stays valid through the whole redial window.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Why did my stored transaction ID stop matching the delivery report?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Almost always because it passed through a double somewhere. Voice IDs are 18 or 19 digits, and doubles are exact only up to 16. Store IDs as text, parse JSON losslessly in Node, and check <code>jq<\/code> before using it on IDs.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">How many times will the platform redial an unanswered call?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">At most twice. <code>reDial<\/code> takes 0, 1 or 2, and <code>redialInterval<\/code> takes 5, 10 or 15 minutes, so the last attempt can start up to 30 minutes after the first.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Can I change or cancel a scheduled voice call?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Yes. <code>VoiceApi\/updateSchedule<\/code> moves it and <code>VoiceApi\/cancelSchedule<\/code> cancels it, both keyed by <code>uuid<\/code>. Read the booking back with <code>VoiceApi\/report\/schedule<\/code> afterwards, and stop making changes shortly before the scheduled time.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Is there a webhook for voice delivery reports?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Not in the Voice Call API reference. Poll <code>VoiceApi\/report\/dlr<\/code> by date range, re-read a trailing window so late updates are picked up, and upsert on <code>msgId<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">How do I get more rows than one report reply returns?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Make the date window smaller. The report requests have no page parameter, so split any window whose <code>pages.last<\/code> is above 1 or whose <code>count.total<\/code> exceeds the rows returned, and merge the halves.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Where can I see which clips are in my audio library?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Not through the API. There&#8217;s no list endpoint for clips, so keep your own registry of <code>libraryId<\/code>, name and source file, and use the dashboard when you need to compare.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Do the daily summary dates use UTC or IST?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">IST. The sample <code>date<\/code> of 1720290600000 is exactly midnight on 7 July 2024 in India Standard Time. Bucket your own counts by IST day before you compare them with the summary.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Can I test voice calls before going live?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Yes. Point the first runs at your own phone, and use the <a href=\"https:\/\/www.smsgatewaycenter.com\/demo\/\">sandbox and demo<\/a> to get a feel for the platform. For voice specifically, three test calls (rejected, answered briefly, answered to the end) teach you more about the delivery rows than any spec.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">Building voice into a product?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Start with a test call to your own phone, then wire up the delivery report sync before you send anything at volume. When you&#8217;re ready to run campaigns or voice OTP in production, <a href=\"https:\/\/www.smsgatewaycenter.com\/contact\/\">talk to the SMSGatewayCenter team<\/a> about routes, pricing and the account settings that fit your use case, or compare plans on the <a href=\"https:\/\/www.smsgatewaycenter.com\/voice-calls-pricing\/\">voice call pricing page<\/a>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">More from our blog<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/messaging-data-retention-erasure-sms-whatsapp-rcs-telegram\/\">Data Retention and Erasure for SMS, WhatsApp, RCS and Telegram: Deleting a Person Without Breaking Opt-Outs<\/a><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/sms-account-security-audit-trail-activity-logs-connected-devices\/\">SMS Account Security Audit Trail: Activity Logs, Login Alerts and Connected Devices via API<\/a><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/sms-whatsapp-preference-centre-design-purposes-channels\/\">Building an SMS and WhatsApp Preference Centre: Purposes, Channels, Frequency and the Send Gate<\/a><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/api-key-rotation-sms-api-without-dropping-messages\/\">API Key Rotation for an SMS API: Rotating a Single-Slot Key Without Dropping Messages<\/a><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/www.smsgatewaycenter.com\/blog\/sms-message-encryption-aes-wire-format-key-handling\/\">SMS Message Encryption: What It Hides, What It Doesn\u2019t, and How to Get the Wire Format Right<\/a><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n","protected":false},"excerpt":{"rendered":"<p>A field-level guide to the SMSGatewayCenter Voice Call API: fourteen operations across nine paths, one call identifier spelled four ways, nineteen-digit IDs that break doubles, an OTP pin typed as an integer, platform redial versus your own retries, and how to page delivery reports when the request has no page parameter.<\/p>\n","protected":false},"author":118,"featured_media":3120,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2010],"tags":[1040,2016,2288,2364,2365,2368,2217,2366,2367,2369],"class_list":["post-3119","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-developer-guides","tag-bulk-voice-call","tag-delivery-reports","tag-json-big-integers","tag-obd-api","tag-openapi","tag-outbound-dialing","tag-smsgatewaycenter-api","tag-voice-call-api","tag-voice-otp-api","tag-voiceapi"],"_links":{"self":[{"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/posts\/3119","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=3119"}],"version-history":[{"count":0,"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/posts\/3119\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/media\/3120"}],"wp:attachment":[{"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/media?parent=3119"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/categories?post=3119"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.smsgatewaycenter.com\/blog\/wp-json\/wp\/v2\/tags?post=3119"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}