Services and pricing
A Service defines your prepaid SMS pricing, minimum deposit, HTTP and SMPP submission limits, and maximum simultaneous SMPP bindings. Choose a Service for your operational needs: Services with lower rates may require higher minimum deposits. The default Service is marked Recommended, not necessarily the cheapest.
Flexible prepaid messaging
- The minimum deposit applies to every top-up, not just your first payment.
- A deposit becomes account credit, not a fixed SMS bundle.
- No expiry: unused deposited credit remains available.
- No monthly commitment: there is no recurring purchase obligation or mandatory monthly SMS volume.
- Pay for actual usage: your balance is debited for submitted SMS segments at the applicable destination price.
- HTTP and SMPP use the same account balance and Service.
Prices are charged per SMS segment. Long or Unicode messages may use multiple segments. The minimum deposit applies to every top-up for the selected Service.
Current public pricing
Country-wide prices. Configured network-specific prices take precedence. Selecting a country does not guarantee availability.
Standard
Recommended
Standard SMS Service.
USD 0.030000 per SMS segment
Country-wide rate
- Minimum deposit for every top-up
- USD 0.00
- HTTP throughput
- 10 submissions / second
- SMPP throughput
- 10 submissions / second
- Simultaneous SMPP bindings
- 2
Business
Business SMS service.
USD 0.025000 per SMS segment
Country-wide rate
- Minimum deposit for every top-up
- USD 250.00
- HTTP throughput
- 10 submissions / second
- SMPP throughput
- 10 submissions / second
- Simultaneous SMPP bindings
- 2
Enterprise
Emterprise SMS service.
USD 0.022000 per SMS segment
Country-wide rate
- Minimum deposit for every top-up
- USD 1000.00
- HTTP throughput
- 10 submissions / second
- SMPP throughput
- 10 submissions / second
- Simultaneous SMPP bindings
- 2
Prices are charged per SMS segment. Long or Unicode messages may use multiple segments. The minimum deposit applies to every top-up for the selected Service.
Public endpoints
Base URL: https://sms.notrel.net/api/public/v1/messaging
These endpoints require no authentication. Send Accept: application/json.
They only retrieve public information; these examples do not send messages or
make purchases. Catalogue order, names, prices, limits, deposits, and registration
URLs can change: always read the returned values rather than copying them into
your application.
List Services
GET /services has no required query parameters. The response is a JSON object
with a data array of public Services; an empty array means no public Services
are currently listed.
curl --request GET \
--url 'https://sms.notrel.net/api/public/v1/messaging/services' \
--header 'Accept: application/json'
Country-wide rates
GET /rates?country=CD returns a JSON object with a data array of effective
rates for the requested destination, one per returned Service.
curl --request GET \
--url 'https://sms.notrel.net/api/public/v1/messaging/rates?country=CD' \
--header 'Accept: application/json'
Network rates
curl --request GET \
--url 'https://sms.notrel.net/api/public/v1/messaging/rates?country=CD&mccmnc=63002' \
--header 'Accept: application/json'
| Query parameter | Required | Meaning |
|---|---|---|
country |
Yes, for /rates |
ISO 3166-1 alpha-2 destination code; CD is the Democratic Republic of the Congo. |
mccmnc |
No | Mobile country code and mobile network code, supplied together as a string, such as 63002. Preserve any leading zeros. |
Without mccmnc, the API returns country-wide pricing. When it is supplied,
a configured exact network rate takes precedence. If no exact network rate
exists, the API falls back to the country rate automatically. Check scope
to determine which rate was returned; do not implement a different fallback
or infer network pricing from the request alone.
Response fields
Both endpoints return { "data": [...] }. Join the collections with
service.slug === rate.service, not their positions in the arrays. Preserve
the catalogue order and use each Service’s signup_url unchanged when linking
to registration so its Service selection is retained.
Service fields
| Field | Type | Meaning |
|---|---|---|
slug |
string | Stable public Service identifier; matches the rate’s service. |
name |
string | Public Service name. |
description |
string | Public description. |
currency |
string | Service currency; display it beside amounts. |
default |
boolean | Whether this is the default Service; not a claim that its price is lowest. |
minimum_deposit |
decimal string | Minimum required for every prepaid top-up. |
limits.http_tps |
integer | HTTP submission limit per second. |
limits.smpp_tps |
integer | SMPP submission limit per second. |
limits.maximum_bindings |
integer | Maximum simultaneous customer SMPP bindings. |
signup_url |
HTTPS URL | Registration URL that preselects this Service. |
Rate fields
| Field | Type | Meaning |
|---|---|---|
service |
string | Public Service slug used to join with the catalogue. |
country |
string | ISO alpha-2 destination code. |
mccmnc |
string or null | Selected mobile-network identifier, or null for a country lookup. |
available |
boolean | Whether an effective rate exists. If false, show Contact us or Not currently available, never a numeric price. |
scope |
string | country or network, identifying the effective rate’s scope. |
price_per_segment |
decimal string when available | Effective charge for one SMS segment, with six decimal places. Do not use it when available is false. |
currency |
string | Rate currency; it must match the joined Service before comparing or displaying its price. |
Keep monetary fields as strings for display. Do not use JavaScript Number or
parseFloat, PHP float casts, or Python float for currency calculations.
Use a decimal arithmetic library or exact scaled integers if calculating money;
the examples below simply retain the original strings. Throughput and binding
limits are not a delivery guarantee.
Loading, caching, and errors
- Fetch the catalogue and destination rates before showing prices. While a new destination loads, replace old prices with a loading state so they cannot be mistaken for the new destination’s rates.
- Cache successful responses for at most five minutes, or less when the
response’s
Cache-Controlrequires it. Respectno-storeandno-cache. - Store each endpoint/query combination separately. Retain its
ETagand send it asIf-None-Matchwhen revalidating. A304 Not Modifiedresponse has no new body: reuse the matching saved body and apply the new cache headers. - A retained body must not be displayed past its freshness period without successful revalidation. Never substitute example or stale prices after an API failure.
- For an empty catalogue, show an empty state and a contact action. A missing
rate,
available: false, or conflicting currency means no displayable price for that Service; do not turn it into a zero price. - Timeouts, non-success HTTP responses, or malformed payloads should show Pricing temporarily unavailable and a contact/retry action. Do not let pricing failure break the rest of the page.
- Validate
countryandmccmncbefore requesting rates. Handle non-success responses without assuming a specific error-body schema. Do not retry indefinitely; retry later or let the visitor request a refresh.
The examples below perform fresh, uncached requests with bounded timeouts. Add conditional caching as described above for production traffic. In the website, prices are fetched on the server and revalidated within five minutes; the destination form in this guide also works without JavaScript.
JavaScript
Run this in a server-side JavaScript environment supporting fetch and
AbortSignal.timeout. The optional network argument adds mccmnc.
const base = 'https://sms.notrel.net/api/public/v1/messaging';
async function readData(url) {
const response = await fetch(url, {
headers: { Accept: 'application/json' },
signal: AbortSignal.timeout(5000),
cache: 'no-store',
});
if (!response.ok) throw new Error(`Pricing request failed: ${response.status}`);
const body = await response.json();
if (!Array.isArray(body.data)) throw new Error('Invalid pricing response');
return body.data;
}
async function pricing(country = 'CD', network = null) {
const query = new URLSearchParams({ country });
if (network !== null) query.set('mccmnc', network);
const [services, rates] = await Promise.all([
readData(`${base}/services`),
readData(`${base}/rates?${query}`),
]);
return services.map(service => {
const rate = rates.find(rate => rate.service === service.slug && rate.country === country);
const available = rate?.available === true && rate.currency === service.currency;
return {
service,
price: available ? rate.price_per_segment : null, // Keep decimal string.
scope: available ? rate.scope : null,
};
});
}
pricing().then(console.log).catch(() => console.error('Pricing temporarily unavailable'));
PHP
Requires PHP’s cURL extension. The decimal strings remain strings after JSON decoding; do not cast them to floats.
<?php
$base = 'https://sms.notrel.net/api/public/v1/messaging';
function readData(string $url): array {
$curl = curl_init($url);
curl_setopt_array($curl, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Accept: application/json'],
CURLOPT_CONNECTTIMEOUT => 2,
CURLOPT_TIMEOUT => 5,
]);
$body = curl_exec($curl);
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
curl_close($curl);
if ($body === false || $status < 200 || $status >= 300) {
throw new RuntimeException('Pricing request failed');
}
$payload = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
if (!isset($payload['data']) || !is_array($payload['data'])) {
throw new RuntimeException('Invalid pricing response');
}
return $payload['data'];
}
try {
$country = 'CD';
$query = ['country' => $country];
// Optional network lookup: $query['mccmnc'] = '63002';
$services = readData($base.'/services');
$rates = readData($base.'/rates?'.http_build_query($query));
$result = [];
foreach ($services as $service) {
$matches = array_values(array_filter($rates, fn ($rate) =>
$rate['service'] === $service['slug'] && $rate['country'] === $country));
$rate = $matches[0] ?? null;
$available = ($rate['available'] ?? false) === true
&& $rate['currency'] === $service['currency'];
$result[] = [
'service' => $service,
'price' => $available ? $rate['price_per_segment'] : null,
'scope' => $available ? $rate['scope'] : null,
];
}
echo json_encode($result, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT);
} catch (Throwable $error) {
echo 'Pricing temporarily unavailable';
}
Python
This example uses only Python’s standard library. Monetary values decoded from JSON strings are retained as strings.
import json
from urllib.parse import urlencode
from urllib.request import Request, urlopen
BASE = 'https://sms.notrel.net/api/public/v1/messaging'
def read_data(url):
request = Request(url, headers={'Accept': 'application/json'})
with urlopen(request, timeout=5) as response:
payload = json.load(response)
if not isinstance(payload.get('data'), list):
raise ValueError('Invalid pricing response')
return payload['data']
def pricing(country='CD', network=None):
query = {'country': country}
if network is not None:
query['mccmnc'] = network
services = read_data(f'{BASE}/services')
rates = read_data(f'{BASE}/rates?{urlencode(query)}')
result = []
for service in services:
rate = next((r for r in rates if r['service'] == service['slug']
and r['country'] == country), None)
available = (rate is not None and rate.get('available') is True
and rate.get('currency') == service['currency'])
result.append({
'service': service,
'price': rate['price_per_segment'] if available else None,
'scope': rate['scope'] if available else None,
})
return result
try:
print(json.dumps(pricing(), indent=2))
except (OSError, ValueError, KeyError, TypeError):
print('Pricing temporarily unavailable')