Skip to main content

SMSc - SMS Service Center Application

SMS Center application built with Phoenix/Elixir, providing a centralized message queue and REST API for SMS routing and delivery.

Documentation

Core Documentation

Compliance Documentation

Performance

Architecture Overview

SMS_C Core provides a protocol-agnostic message queue and REST API. External SMSC frontends (SMPP, IMS, SS7/MAP) connect as standalone gateways that communicate with the core via REST API.

Message Flow

Outbound Message Flow (MT - Mobile Terminated)

Inbound Message Flow (MO - Mobile Originated)

Key Features

1. Protocol-Agnostic Design

  • SMS_C Core handles message persistence, routing, and API
  • External frontends (SMPP, IMS, SS7/MAP) handle protocol-specific communication
  • All frontends communicate via unified REST API
  • Add new protocols without changing core
  • Each can be scaled independently

2. Message Routing

  • Dynamic routing engine with runtime configuration
  • Prefix-based routing (calling/called numbers)
  • SMSC and source type filtering (IMS/Circuit Switched/SMPP)
  • Priority-based routing with weight-based load balancing
  • Auto-reply routing and message drop capabilities
  • Charging control per route
  • Web UI for route management
  • Real-time route updates without service interruption

📖 See SMS Routing Guide for comprehensive documentation

3. Retry Logic with Exponential Backoff

  • Automatic retry on delivery failure
  • Exponential backoff: 1min, 2min, 4min, 8min, etc.
  • Configurable maximum retry attempts
  • Message expiration handling
  • Per-message retry tracking

Operations Guide

Access Points:

  • REST API: https://localhost:8443 (or http://localhost:8080 without TLS)
  • Control Panel: https://localhost:8086
  • API Documentation (Swagger UI): https://localhost:8443/api/docs

Start External Frontends: Each protocol frontend is a standalone application. See individual frontend documentation for startup instructions.

Configuration

All configuration is managed directly in config/runtime.exs. No environment variables are used.

Core Configuration

No core application configuration environment variables are currently used. Server ports and hostnames are configured in config/runtime.exs:

  • API Server: Port 8443 (HTTPS), listening on 0.0.0.0
  • Control Panel: Port 80 (HTTP), listening on 0.0.0.0

Database Configuration

Database settings are configured in config/runtime.exs:

  • Username: omnitouch
  • Password: omnitouch2024
  • Hostname: localhost
  • Port: 3306
  • Database name: smsc_new
  • Pool size: 1

Cluster Configuration

Cluster settings are configured in config/runtime.exs:

  • Cluster nodes: "" (empty - no clustering by default)
  • DNS cluster query: nil

Message Queue Configuration

Message queue settings are configured in config/runtime.exs:

  • Dead letter time: 1440 minutes (24 hours before message expires)

Charging Integration

Charging settings are configured in config/runtime.exs:

  • URL: http://localhost:2080/jsonrpc
  • Tenant: mnc057.mcc505.3gppnetwork.org
  • Destination: 55512341234
  • Source: 00101900000257
  • Subject: 00101900000257
  • Account: 00101900000257

SMS Routing Configuration

The SMS routing system uses dynamic, database-backed routes that can be managed via Web UI or configuration file. Routes are loaded from config/runtime.exs on first startup.

Configuration Example:

config :sms_c, :sms_routes, [
%{
called_prefix: "+44",
dest_smsc: "InternationalGW",
weight: 100,
priority: 100,
description: "UK International SMS",
enabled: true
},
%{
called_prefix: "1900",
dest_smsc: "PremiumGW",
charged: :yes,
priority: 50,
description: "US Premium Numbers",
enabled: true
}
]

Features:

  • Prefix-based matching (calling/called numbers)
  • Source SMSC and type filtering
  • Priority and weight-based routing
  • Auto-reply and drop capabilities
  • Per-route charging control
  • Runtime management via Web UI at /routing

📖 See SMS Routing Guide for complete documentation, examples, and API reference.

REST API Endpoints

Message Queue Operations

Submit SMS (Create Message)

POST /api/messages
Content-Type: application/json

{
"source_msisdn": "+1234567890",
"destination_msisdn": "+0987654321",
"message_body": "Hello, World!",
"source_smsc": "web-app",
"dest_smsc": "smpp-provider", # Optional - routing engine assigns if null
"tp_dcs_character_set": "gsm7", # Optional: gsm7, 8bit, latin1, ucs2
"tp_dcs_coding_group": "general_data_coding",
"expires": "2025-10-17T10:30:00Z" # Optional - defaults to 24h from now
}

Required Fields:

  • destination_msisdn - Destination phone number
  • message_body - Message text content
  • source_msisdn - Source phone number
  • source_smsc - Source system identifier

Optional Fields:

  • dest_smsc - Destination gateway (routing engine assigns if not provided)
  • source_imsi, dest_imsi - IMSI identifiers
  • tp_dcs_character_set - Character encoding (gsm7, 8bit, latin1, ucs2)
  • tp_dcs_coding_group - DCS coding group
  • tp_dcs_compressed - Compression flag (boolean)
  • tp_dcs_has_message_class - Message class flag (boolean)
  • tp_dcs_message_class - Message class value
  • tp_user_data_header - User data header (map)
  • message_part_number, message_parts - Multipart message fields
  • expires - Expiration timestamp (defaults to 24 hours)
  • deliver_after - Delayed delivery timestamp
  • deadletter, raw_data_flag, raw_sip_flag - Boolean flags

Response:

{
"status": "success",
"data": {
"id": 123,
"source_msisdn": "+1234567890",
"destination_msisdn": "+0987654321",
"dest_smsc": "smpp-provider",
"message_body": "Hello, World!",
"deliver_time": null,
"delivery_attempts": 0,
"expires": "2025-10-17T10:30:00Z",
"inserted_at": "2025-10-16T10:30:00Z"
}
}

Get Messages for SMSC

GET /api/messages/get_by_smsc?smsc=my-smsc-name

Returns all undelivered messages where:

  • destination_smsc is null OR matches the provided SMSC name
  • Message is not expired
  • Ready to send (deliver_after is null or in the past)

Response:

{
"status": "success",
"data": [
{
"id": 123,
"source_msisdn": "+1234567890",
"destination_msisdn": "+0987654321",
"message_body": "Hello",
"destination_smsc": "my-smsc-name",
"delivery_attempts": 0
}
]
}

List Messages with Optional SMSC Filtering

# List all messages in the queue
GET /api/messages

# List messages for specific SMSC (with header filtering)
GET /api/messages
SMSc: my-smsc-name

Without SMSc Header: Returns all messages in the queue regardless of delivery status or expiration.

With SMSc Header: Returns undelivered messages where:

  • dest_smsc matches the header value OR dest_smsc is null
  • deliver_time is null (not yet delivered)
  • deliver_after is null or before/equal to current time (ready to deliver)
  • expires is after current time (not expired)
  • Ordered by insertion time (oldest first)

Note: The SMSc header approach allows external frontends to poll for their messages using the same endpoint pattern, with the header controlling the filtering behavior.

Response:

[
{
"id": 123,
"source_msisdn": "+1234567890",
"destination_msisdn": "+0987654321",
"message_body": "Hello, World!",
"dest_smsc": "my-smsc-name",
"deliver_time": null,
"delivery_attempts": 0,
"expires": "2025-10-17T10:30:00Z",
"inserted_at": "2025-10-16T10:30:00Z"
}
]

Get Single Message

GET /api/messages/{id}

Update Message

PATCH /api/messages/{id}
Content-Type: application/json

{
"status": "delivered",
"delivered_at": "2025-10-16T10:30:00Z"
}

Delete SMS

DELETE /api/messages/{id}

Handle Failed Delivery (Increment Retry Counter)

When a message delivery fails temporarily, increment the delivery attempt counter and schedule retry with exponential backoff.

Method 1: Using PUT (Recommended)

# Simple and semantic - PUT indicates updating delivery state
PUT /api/messages/{id}

Method 2: Using Explicit Endpoint

# Alternative explicit endpoint
POST /api/messages/{id}/increment_delivery_attempt

Both methods increment delivery_attempts and set exponential backoff delay via deliver_after:

AttemptBackoff FormulaDelayTotal Time
1st2^1 minutes2 min2 min
2nd2^2 minutes4 min6 min
3rd2^3 minutes8 min14 min
4th2^4 minutes16 min30 min
5th2^5 minutes32 min1h 2min
6th2^6 minutes64 min2h 6min

Response:

{
"id": 123,
"delivery_attempts": 1,
"deliver_after": "2025-10-20T19:05:00Z",
"deliver_time": null,
"expires": "2025-10-21T19:03:00Z",
...
}

Note: Messages with future deliver_after are automatically filtered from GET requests until the backoff period expires.

Update Message (Partial Update)

For updating specific message fields (unchanged behavior):

PATCH /api/messages/{id}
Content-Type: application/json

{
"dest_smsc": "updated-gateway",
"status": "delivered"
}

Important: PUT and PATCH behave differently:

  • PUT → Increments delivery attempts with backoff (no body required)
  • PATCH → Performs partial field updates (body required)

Frontend Health Tracking

The SMS_C Core tracks the health and availability of external frontends through a registration system. This allows monitoring of frontend uptime, detecting failures, and maintaining historical availability data.

Note: Frontend registration is NOT used for message delivery or routing. Messages are routed based on the dest_smsc field. The registration system exists solely for health monitoring and operational visibility.

How Frontend Registration Works

Each external frontend (SMPP, IMS, SS7/MAP gateway) periodically sends a heartbeat registration to the SMS_C Core:

  1. Heartbeat Interval: Frontends should register every 30-60 seconds
  2. Expiration Time: Registrations expire after 90 seconds without an update
  3. Automatic State Management:
    • New frontends create a new registration record
    • Existing active frontends update their registration (extends expiration)
    • Expired frontends that come back online create a new registration period

Frontend Registration Endpoints

Register/Update Frontend (Heartbeat)
POST /api/frontends
Content-Type: application/json

{
"frontend_name": "smpp-gateway-1",
"frontend_type": "SMPP",
"ip_address": "10.0.1.5",
"hostname": "smpp-gw-01",
"uptime_seconds": 3600,
"configuration": "{\"port\": 2775, \"system_id\": \"smpp_user\"}"
}

Required Fields:

  • frontend_name - Unique identifier for the frontend instance
  • frontend_type - Type of frontend (SMPP, IMS, MAP, etc.)

Optional Fields:

  • ip_address - IP address of the frontend (auto-detected from request source if not provided)
  • hostname - Hostname of the frontend server
  • uptime_seconds - Seconds since the frontend started
  • configuration - JSON string with frontend-specific config

Note: If ip_address is not provided, the SMS_C Core will automatically use the source IP of the HTTP request. This works with both direct connections and proxied requests (via X-Forwarded-For header).

Response:

{
"id": 42,
"frontend_name": "smpp-gateway-1",
"frontend_type": "SMPP",
"ip_address": "10.0.1.5",
"hostname": "smpp-gw-01",
"uptime_seconds": 3600,
"status": "active",
"last_seen_at": "2025-10-20T10:30:00Z",
"expires_at": "2025-10-20T10:31:30Z",
"inserted_at": "2025-10-20T10:00:00Z"
}
List All Frontend Registrations
GET /api/frontends

Returns all frontend registrations (active and expired), ordered by most recent activity.

List Active Frontends Only
GET /api/frontends/active

Returns only currently active (non-expired) frontends.

Get Frontend Statistics
GET /api/frontends/stats

Returns summary statistics:

{
"active": 5,
"expired": 12,
"unique_frontends": 8
}
Get Frontend History
GET /api/frontends/history/{frontend_name}

Returns all historical registrations for a specific frontend, useful for analyzing uptime/downtime patterns.

Example:

GET /api/frontends/history/smpp-gateway-1
Get Specific Registration
GET /api/frontends/{id}

Implementation in External Frontends

External frontends should implement a background task that sends heartbeats:

Example (pseudocode):

import time
import requests

def send_heartbeat():
"""Send heartbeat every 30 seconds"""
while True:
try:
data = {
"frontend_name": "my-smpp-gateway",
"frontend_type": "SMPP",
"ip_address": get_local_ip(),
"hostname": get_hostname(),
"uptime_seconds": get_uptime()
}

response = requests.post(
"https://smsc-core:8443/api/frontends",
json=data,
timeout=5
)

if response.status_code in [200, 201]:
logger.debug("Heartbeat sent successfully")
else:
logger.error(f"Heartbeat failed: {response.status_code}")

except Exception as e:
logger.error(f"Heartbeat error: {e}")

time.sleep(30) # Send every 30 seconds

# Start heartbeat in background thread
threading.Thread(target=send_heartbeat, daemon=True).start()

Monitoring Frontend Health

Control Panel - The web UI at https://localhost:8086 shows:

  • Currently active frontends
  • Last seen timestamp for each frontend
  • Uptime tracking
  • Historical availability

API Queries:

# Get all active frontends
curl https://localhost:8443/api/frontends/active

# Check if specific frontend is up
curl https://localhost:8443/api/frontends/history/smpp-gateway-1 | jq '.[0].status'

# Get health statistics
curl https://localhost:8443/api/frontends/stats

Other Endpoints

Status

GET /api/status

Locations

GET /api/locations
POST /api/locations
GET /api/locations/{id}
PATCH /api/locations/{id}
DELETE /api/locations/{id}

SS7 Events

GET /api/ss7_events
POST /api/ss7_events
GET /api/ss7_events/{id}
PATCH /api/ss7_events/{id}
DELETE /api/ss7_events/{id}

MMS Message Queue

GET /api/mms_message_queues
POST /api/mms_message_queues
GET /api/mms_message_queues/{id}
PATCH /api/mms_message_queues/{id}
DELETE /api/mms_message_queues/{id}

Performance

The SMS_C Core delivers exceptional throughput using Mnesia for in-memory message storage with automatic archiving to SQL for long-term CDR retention.

Benchmark Results

Measured on Intel i7-8650U @ 1.90GHz (8 cores):

Message Insert Performance:

  • insert_message (with routing): 1,750 msg/sec (0.58ms avg latency)
  • insert_message (simple): 1,750 msg/sec (0.57ms avg latency)
  • ~150 million messages per day capacity

Query Performance:

  • get_messages_for_smsc: 800 msg/sec (1.25ms avg)
  • list_message_queues: Fast in-memory access
  • Memory usage: 62 KB per insert operation

Architecture

Storage Strategy:

  • Active Messages: Stored in Mnesia (in-memory + disk) for ultra-fast access
  • Message Archive: Automatically archived to SQL for long-term CDR storage
  • Retention: Configurable retention period (default: 24 hours)
  • No SQL bottleneck: All active message operations bypass SQL

Configuration

Message storage and retention configured in config/runtime.exs:

config :sms_c,
message_retention_hours: 24, # Archive messages older than 24 hours
batch_insert_batch_size: 100, # CDR batch size for SQL archiving
batch_insert_flush_interval_ms: 100 # CDR flush interval

For detailed tuning guidance, see: docs/PERFORMANCE_TUNING.md

Monitoring

Control Panel - Web UI at https://localhost:8086

  • View message queue
  • Submit test messages
  • Manage SMS routing (see Routing Guide)
  • Simulate routing decisions
  • View system resources
  • Track batch worker statistics

Batch Worker Statistics:

# Get current batch worker stats
SmsC.Messaging.BatchInsertWorker.stats()

Returns:

%{
total_enqueued: 10000,
total_flushed: 9900,
current_queue_size: 100,
last_flush_duration_ms: 45
}

Logs - Application logs written to stdout

# View logs in real-time
tail -f log/dev.log

Troubleshooting

Port Already in Use

# Find process using port
lsof -i :4000

# Kill the process
kill -9 <PID>

External Frontend Not Connecting

Symptoms: Messages stuck in queue, frontend logs show connection errors

Check:

  • Verify API_BASE_URL is correctly set in frontend
  • Check SMS_C Core is running and accessible
  • Review network/firewall rules
  • Verify frontend configuration

Solution:

# Test API connectivity from frontend
curl http://localhost:4000/api/status

# Restart frontend
export API_BASE_URL="http://localhost:4000"
# Start frontend application

Messages Not Being Delivered

Symptoms: Messages remain undelivered, retry attempts incrementing

Check:

  1. Frontend logs for send errors
  2. External network connectivity
  3. Frontend configuration (credentials, addresses)
  4. Message format compatibility

View failed messages:

# Get messages with retry attempts
curl https://localhost:8443/api/messages | jq '.data[] | select(.delivery_attempts > 0)'

High Message Latency

Symptoms: Messages taking longer than expected, queue backlog

Check:

  1. Frontend polling interval (may need to decrease for more frequent polling)
  2. Database performance
  3. Network latency to external systems

Monitor queue depth:

watch -n 5 'curl -s https://localhost:8443/api/messages | jq ".data | length"'