International SMS

7 views Markdown

The fifteen endpoints that manage international SMS sender name applications, country prices and module settings.

Overview

Some countries insist the SMS sender name is approved in advance. The client applies with documents and you approve or refuse; the first six endpoints run that flow.

The rest is about the selling: which module sends, the cost and price per country, the margin, and clearing old records. Prices are kept in a single primary currency, and automatic pricing does not run until it is set.

Reference

Listing the Applications

get/api/v1/admin/products/sms/intl-origins
Products/GetSmsIntlOrigins admin paged

Returns the sender name applications clients opened, one per country.

Query parameters 3
searchstringSearches the records.
pageintDefaults to 1.
limitintDefaults to 25, maximum 100.
Response fields data[] — 11
idintRecord id.
origin_idintId of the domestic sender name it comes from.
origin_namestringThe sender name.
country_codestringThe country code. Two letters, lower case.
statusstringwaiting is awaiting review, active was approved, inactive was refused.
status_messagestringThe message left on the decision.
clientobjectThe client who opened the request.
idintClient id.
full_namestringFirst and last name.
company_namestringCompany name.
attachmentsarrayThe documents the client uploaded.
sizeintThe file size in bytes.
file_namestringThe original name the client uploaded.
namestringThe name stored on the server.
file_pathstringWhere the file is kept.
created_atstring | nullWhen the request was opened.
approved_datestring | nullWhen it was approved.
rejected_datestring | nullWhen it was refused.
Meta 4
totalintTotal records matching the filter.
pageintThe page you are on.
limitintThe page size.
next_pageintThe next page. Zero means you are on the last one.
Errors 1
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/admin/products/sms/intl-origins' \
  -H "Authorization: Bearer $API_KEY"
const res  = await fetch('https://panel.example.com/api/v1/admin/products/sms/intl-origins', {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/products/sms/intl-origins');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
$response = Api::Products()->GetSmsIntlOrigins();

$waiting = array_filter(
    $response['data'],
    fn (array $r): bool => $r['status'] === 'waiting',
);
Response
{
  "data": [
    {
      "id": 9,
      "origin_id": 1,
      "origin_name": "ACME",
      "country_code": "sv",
      "status": "waiting",
      "status_message": "",
      "client": { "id": 2, "full_name": "John Doe", "company_name": "" },
      "attachments": [
        {
          "size": 255435,
          "file_name": "licence.jpg",
          "name": "215d8151c44f2d058eee5e5.jpg",
          "file_path": "215d8151c44f2d058eee5e5.jpg"
        }
      ],
      "created_at": "2026-03-09 11:52:45",
      "approved_date": null,
      "rejected_date": null
    }
  ],
  "meta": { "total": 2, "page": 1, "limit": 25, "next_page": 0 }
}

Application Detail

get/api/v1/admin/products/sms/intl-origins/{id}
Products/GetSmsIntlOrigin admin

Returns one application. The schema is the same as a list item.

Response fields data — 11
idintRecord id.
origin_idintId of the domestic sender name it comes from.
origin_namestringThe sender name.
country_codestringThe country code. Two letters, lower case.
statusstringwaiting is awaiting review, active was approved, inactive was refused.
status_messagestringThe message left on the decision.
clientobjectThe client who opened the request.
idintClient id.
full_namestringFirst and last name.
company_namestringCompany name.
attachmentsarrayThe documents the client uploaded.
sizeintThe file size in bytes.
file_namestringThe original name the client uploaded.
namestringThe name stored on the server.
file_pathstringWhere the file is kept.
created_atstring | nullWhen the request was opened.
approved_datestring | nullWhen it was approved.
rejected_datestring | nullWhen it was refused.
Errors 2
not_found404No such record.
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/admin/products/sms/intl-origins/9' \
  -H "Authorization: Bearer $API_KEY"
const res  = await fetch('https://panel.example.com/api/v1/admin/products/sms/intl-origins/9', {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/products/sms/intl-origins/9');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
$response = Api::Products()->GetSmsIntlOrigin(['id' => 9]);

Settling an Application

put/api/v1/admin/products/sms/intl-origins/{id}/status
Products/SetSmsIntlOriginStatus admin

Approves or refuses the application.

Body 2
actionstringrequiredactive approves, inactive refuses.
reasonstringThe message to leave on the decision. Canned texts come from the reasons endpoint.
Response fields data
dataobjectThe application as it now stands. Same shape as the detail schema. Of the two decision dates, the one that no longer applies is cleared.
Errors 3
not_found404No such record.
invalid_action422The action is neither active nor inactive.
insufficient_scope403The key lacks the required scope.
Request
curl -X PUT 'https://panel.example.com/api/v1/admin/products/sms/intl-origins/9/status' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"action":"inactive","reason":"Documents missing"}'
const res = await fetch('https://panel.example.com/api/v1/admin/products/sms/intl-origins/9/status', {
  method: 'PUT',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ action: 'inactive', reason: 'Documents missing' }),
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/products/sms/intl-origins/9/status');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'PUT',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'action' => 'inactive',
        'reason' => 'Documents missing',
    ]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// Take the canned reason from the template list so the client always sees the same text.
$reasons = Api::Products()->GetSmsReasons([], ['group' => 'international-sms'])['data'];

Api::Products()->SetSmsIntlOriginStatus([
    'id'     => 9,
    'action' => 'inactive',
    'reason' => $reasons['en'][0]['description'] ?? '',
]);

Settling in Bulk

post/api/v1/admin/products/sms/intl-origins/bulk
Products/BulkSmsIntlOrigins admin no reason

Approves or refuses several applications in one call.

Body 2
idsint[]requiredThe application ids.
actionstringrequiredactive or inactive.
Response fields data — 2
updatedint[]The ids whose status changed.
actionstringThe status that was applied.
Errors 3
ids_required422ids was empty.
invalid_action422The action is neither active nor inactive.
insufficient_scope403The key lacks the required scope.
Request
curl -X POST 'https://panel.example.com/api/v1/admin/products/sms/intl-origins/bulk' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"ids":[9,10],"action":"active"}'
const res = await fetch('https://panel.example.com/api/v1/admin/products/sms/intl-origins/bulk', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ ids: [9, 10], action: 'active' }),
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/products/sms/intl-origins/bulk');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode(['ids' => [9, 10], 'action' => 'active']),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// The bulk endpoint has NO reason field: settle one by one when a refusal message is needed.
Api::Products()->BulkSmsIntlOrigins([
    'ids'    => [9, 10],
    'action' => 'active',
]);

Deleting an Application

delete/api/v1/admin/products/sms/intl-origins/{id}
Products/DeleteSmsIntlOrigin admin documents go too

Deletes the application and the documents uploaded with it.

Response fields data — 2
deletedboolWhether the delete succeeded.
idintId of the deleted application.
Errors 2
not_found404No such record.
insufficient_scope403The key lacks the required scope.
Request
curl -X DELETE 'https://panel.example.com/api/v1/admin/products/sms/intl-origins/9' \
  -H "Authorization: Bearer $API_KEY"
const res = await fetch('https://panel.example.com/api/v1/admin/products/sms/intl-origins/9', {
  method: 'DELETE',
  headers: { Authorization: `Bearer ${apiKey}` },
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/products/sms/intl-origins/9');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'DELETE',
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
$response = Api::Products()->DeleteSmsIntlOrigin(['id' => 9]);

A Delivery Report

get/api/v1/admin/products/sms/intl-reports/{id}
Products/GetSmsIntlReport admin asks the module

Asks the provider for the live delivery state of a sent message.

Response fields data — 5
modulestringThe SMS module that produced the report.
sendingarrayRecipients still in flight. The row shape belongs to the module, so it differs from one provider to the next.
deliveredarrayRecipients confirmed delivered.
failedarrayRecipients that failed.
total_recipientsintThe count the module reports. Read this one rather than the lengths of the three arrays; they can differ.
Errors 5
not_found404No such report.
no_module422There is no SMS module for this report.
not_supported422The module does not support delivery reports.
report_failed422The provider did not answer.
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/admin/products/sms/intl-reports/501' \
  -H "Authorization: Bearer $API_KEY"
const res  = await fetch('https://panel.example.com/api/v1/admin/products/sms/intl-reports/501', {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/products/sms/intl-reports/501');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// The report is produced by asking the provider: it can be slow and depends on module support.
$response = Api::Products()->GetSmsIntlReport(['id' => 501]);

Listing the Refusal Reasons

get/api/v1/admin/products/sms/reasons
Products/GetSmsReasons admin

Returns the canned texts used when refusing an application, one set per language.

Query parameters 1
groupstringWhich channel: sms or international-sms. It defaults to sms, so forgetting the parameter gives you the domestic list.
Response fields data
dataobjectA map from language code to a list of reasons. Each carries a title and a description.
Errors 1
insufficient_scope403The key lacks the required scope.
Request
curl -G 'https://panel.example.com/api/v1/admin/products/sms/reasons' \
  -H "Authorization: Bearer $API_KEY" \
  -d group=international-sms
const url = new URL('https://panel.example.com/api/v1/admin/products/sms/reasons');
url.searchParams.set('group', 'international-sms');

const res  = await fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
const body = await res.json();
$url = 'https://panel.example.com/api/v1/admin/products/sms/reasons?' . http_build_query(['group' => 'international-sms']);

$ch = curl_init($url);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
$response = Api::Products()->GetSmsReasons([], ['group' => 'international-sms']);

Adding a Refusal Reason

post/api/v1/admin/products/sms/reasons
Products/AddSmsReason admin 201

Adds a canned refusal text to one language.

Body 2
langstringrequiredWhich language the text goes into.
reasonstringrequiredThe reason text. The title is built from its first fifty characters.
Query parameters 1
groupstringWhich channel: sms or international-sms. It defaults to sms, so forgetting the parameter gives you the domestic list.
Response fields data
dataobjectThe whole reason set after the write, returned with 201. Same shape as the listing: one key per installed language, each holding its own list. A language with no reasons comes back as an empty array.
Errors 2
reason_required422The language or the text was empty.
insufficient_scope403The key lacks the required scope.
Request
curl -X POST 'https://panel.example.com/api/v1/admin/products/sms/reasons?group=international-sms' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"lang":"en","reason":"Please upload a valid trade licence."}'
const res = await fetch('https://panel.example.com/api/v1/admin/products/sms/reasons?group=international-sms', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    lang: 'en',
    reason: 'Please upload a valid trade licence.',
  }),
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/products/sms/reasons?group=international-sms');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'lang'   => 'en',
        'reason' => 'Please upload a valid trade licence.',
    ]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
$response = Api::Products()->AddSmsReason(
    ['lang' => 'en', 'reason' => 'Please upload a valid trade licence.'],
    ['group' => 'international-sms'],
);

Deleting a Refusal Reason

delete/api/v1/admin/products/sms/reasons/{index}
Products/DeleteSmsReason admin deleted by position

Deletes a reason from one language by its position in the list.

Query parameters 2
langstringrequiredWhich language's list to delete from.
groupstringWhich channel: sms or international-sms. It defaults to sms, so forgetting the parameter gives you the domestic list.
Response fields data
dataobjectThe whole reason set after the delete. Same shape as the listing. The list is renumbered, so every reason below the deleted one moves up a position.
Errors 3
lang_required422lang was not given.
not_found404There is no reason at that position.
insufficient_scope403The key lacks the required scope.
Request
curl -X DELETE -G 'https://panel.example.com/api/v1/admin/products/sms/reasons/0' \
  -H "Authorization: Bearer $API_KEY" \
  -d lang=en \
  -d group=international-sms
const url = new URL('https://panel.example.com/api/v1/admin/products/sms/reasons/0');
url.searchParams.set('lang', 'en');
url.searchParams.set('group', 'international-sms');

const res = await fetch(url, {
  method: 'DELETE',
  headers: { Authorization: `Bearer ${apiKey}` },
});

const body = await res.json();
$url = 'https://panel.example.com/api/v1/admin/products/sms/reasons/0?' . http_build_query([
    'lang'  => 'en',
    'group' => 'international-sms',
]);

$ch = curl_init($url);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'DELETE',
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// Positions SHIFT after a delete: going back to front is the safe order for several.
foreach ([2, 1] as $index)
    Api::Products()->DeleteSmsReason(['index' => $index], ['lang' => 'en']);

Reading the Settings

get/api/v1/admin/products/sms/intl-settings
Products/GetSmsIntlSettings admin

Returns the module, pricing and pre-registration settings for international SMS.

Response fields data — 5
active_modulestringThe SMS module in use. none when none is chosen.
cron_statusboolWhether the job that refreshes prices is on.
primary_currencyintCurrency id the prices are kept in.
profit_ratefloatThe profit margin added on top of cost.
pre_register_countriesstring[]The countries where the sender name has to be approved in advance.
Errors 1
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/admin/products/sms/intl-settings' \
  -H "Authorization: Bearer $API_KEY"
const res  = await fetch('https://panel.example.com/api/v1/admin/products/sms/intl-settings', {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/products/sms/intl-settings');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
$response = Api::Products()->GetSmsIntlSettings();

Writing the Settings

put/api/v1/admin/products/sms/intl-settings
Products/SetSmsIntlSettings admin the margin is its own branch

Sets the module, the pricing job and the pre-registration countries, or changes only the margin.

Body 5
marginfloatThe profit margin. Sending this field ignores the others and reprices.
active_modulestringThe SMS module to use.
cron_statusboolTurns on the job that refreshes prices.
primary_currencyintThe currency the prices are kept in.
countries_pre_registerstring[]The countries where the sender name needs approval in advance.
Response fields data
dataobjectThe settings as they now stand. Same shape as reading them.
Errors 3
module_unsupported422The job was asked for but the module cannot fetch prices.
update_failed422The setting could not be stored.
insufficient_scope403The key lacks the required scope.
Request
curl -X PUT 'https://panel.example.com/api/v1/admin/products/sms/intl-settings' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"active_module":"Twilio","cron_status":true,"primary_currency":1}'
const res = await fetch('https://panel.example.com/api/v1/admin/products/sms/intl-settings', {
  method: 'PUT',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    active_module: 'Twilio',
    cron_status: true,
    primary_currency: 1,
  }),
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/products/sms/intl-settings');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'PUT',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'active_module'    => 'Twilio',
        'cron_status'      => true,
        'primary_currency' => 1,
    ]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// With 'margin' in the body the OTHER fields are not read - make them two requests.
Api::Products()->SetSmsIntlSettings(['active_module' => 'Twilio']);
Api::Products()->SetSmsIntlSettings(['margin' => 20]);

Reading the Prices

get/api/v1/admin/products/sms/intl-pricing
Products/GetSmsIntlPricing admin

Returns the cost and the selling price for each country.

Query parameters 1
primary_currencyintUses this instead of the currency in the settings.
Response fields data[] — 5
country_codestringThe country code. UPPER case here, lower case on the application records.
costfloatWhat the provider charges you.
amountfloatWhat the client is charged.
cidintCurrency id.
statusboolWhether sending to that country is open.
Errors 1
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/admin/products/sms/intl-pricing' \
  -H "Authorization: Bearer $API_KEY"
const res  = await fetch('https://panel.example.com/api/v1/admin/products/sms/intl-pricing', {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/products/sms/intl-pricing');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
$response = Api::Products()->GetSmsIntlPricing();

Writing the Prices

put/api/v1/admin/products/sms/intl-pricing
Products/SetSmsIntlPricing admin

Writes the cost and the price per country by hand.

Body 1
valuesobjectrequiredA map from country code to a price object carrying the cost, the amount, the currency and whether sending is open. Only the countries you send change.
Response fields data — 1
updatedintHow many countries were updated.
Errors 2
values_required422values was empty.
insufficient_scope403The key lacks the required scope.
Request
curl -X PUT 'https://panel.example.com/api/v1/admin/products/sms/intl-pricing' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"values":{"US":{"cost":0.01,"amount":0.02,"cid":1,"status":true}}}'
const res = await fetch('https://panel.example.com/api/v1/admin/products/sms/intl-pricing', {
  method: 'PUT',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    values: {
      US: { cost: 0.01, amount: 0.02, cid: 1, status: true },
    },
  }),
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/products/sms/intl-pricing');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'PUT',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'values' => [
            'US' => ['cost' => 0.01, 'amount' => 0.02, 'cid' => 1, 'status' => true],
        ],
    ]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// The country code goes in UPPER case; do not mix it with the lower-case code on applications.
Api::Products()->SetSmsIntlPricing([
    'values' => [
        'US' => ['cost' => 0.01, 'amount' => 0.02, 'cid' => 1, 'status' => true],
    ],
]);

Pulling Prices from the Module

post/api/v1/admin/products/sms/intl-pricing/auto
Products/AutoDefineSmsIntlPricing admin overwrites hand-set prices

Pulls the country prices from the provider and writes them with the margin applied.

Body
No body is needed; send an empty one. The module, the currency and the margin all come from the international SMS settings.
Response fields data — 1
statusstringThe outcome of the run.
Errors 5
no_module422No SMS module is selected.
module_unsupported422The module does not support fetching prices.
no_primary_currency422The primary currency is not set.
auto_define_failed422The prices could not be fetched.
insufficient_scope403The key lacks the required scope.
Request
curl -X POST 'https://panel.example.com/api/v1/admin/products/sms/intl-pricing/auto' \
  -H "Authorization: Bearer $API_KEY"
const res = await fetch('https://panel.example.com/api/v1/admin/products/sms/intl-pricing/auto', {
  method: 'POST',
  headers: { Authorization: `Bearer ${apiKey}` },
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/products/sms/intl-pricing/auto');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// The endpoint has three preconditions: a module is chosen, it can fetch prices, a currency is set.
$settings = Api::Products()->GetSmsIntlSettings()['data'];

if ($settings['active_module'] !== 'none' && $settings['primary_currency'] > 0)
    Api::Products()->AutoDefineSmsIntlPricing();

Clearing the Reports

post/api/v1/admin/products/sms/clear-reports
Products/ClearSmsReports admin cannot be undone

Deletes the SMS records sent on and before the date you give.

Body 2
datestringrequiredThe cut-off date. This date and everything before it goes.
typestringWhich channel: domestic or international. Defaults to domestic.
Response fields data — 3
clearedboolWhether the clear ran.
beforestringThe cut-off date that was used.
typestringThe channel that was cleared.
Errors 2
invalid_date422The date is missing or invalid.
insufficient_scope403The key lacks the required scope.
Request
curl -X POST 'https://panel.example.com/api/v1/admin/products/sms/clear-reports' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"date":"2026-01-01","type":"international"}'
const res = await fetch('https://panel.example.com/api/v1/admin/products/sms/clear-reports', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ date: '2026-01-01', type: 'international' }),
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/products/sms/clear-reports');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'date' => '2026-01-01',
        'type' => 'international',
    ]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// Leave the channel out and the DOMESTIC records go; ask for international explicitly.
Api::Products()->ClearSmsReports([
    'date' => '2026-01-01',
    'type' => 'international',
]);

Pitfalls

The country code case differs between endpoints

The application records return the country code in lower case while the pricing endpoints expect and return upper case. Code that matches the two without normalising the case finds no country at all, and does so quietly.

The margin field ignores the others

With margin in the body only the margin branch runs; the module, the currency and the country list are not read. To change both, send two separate requests.

Auto pricing overwrites hand-set prices

Pulling prices from the module writes over the country prices you set by hand. If you priced a few countries specially you have to enter them again after this call, and with the pricing job on the same overwrite happens on a schedule.

The bulk decision carries no reason

The bulk endpoint only changes the status; there is no field for a refusal message. If the client is meant to see why they were refused, the applications have to be settled one at a time.

Reason positions shift after a delete

Reasons are deleted by their position in the list, not by an id. Deleting one shifts everything after it down by one, so when deleting several go back to front or you will remove the wrong text.

Was this helpful?

Thanks for your feedback!

Still Need Help?

Our support team is here around the clock for anything you can't find above.