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
Get started with Standard →

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
Get started with Business →

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
Get started with Enterprise →

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-Control requires it. Respect no-store and no-cache.
  • Store each endpoint/query combination separately. Retain its ETag and send it as If-None-Match when revalidating. A 304 Not Modified response 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 country and mccmnc before 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')