The full API exposes all gateway capabilities: sending and reading SMS, SIM management, GPS, token statistics, request logs, and modem debug tools.
Default port: 8000 (configure via API_PORT in .env)
Swagger UI: http://<host>:8000/docs
Base URL: http://<host>:8000
All endpoints except GET /health require the X-API-Key header.
Tokens are set in .env before starting the container.
Multiple named tokens (recommended):
API_KEYS=admin:mysecrettoken,monitoring:anothertoken,homeassistant:thirdtokenFormat is name:token pairs separated by commas. The name appears in usage logs and
token statistics — it is never exposed via the API.
Single token (legacy):
API_KEY=mysecrettokenTreated as a token named default.
Generate a strong token:
openssl rand -hex 32X-API-Key: mysecrettoken| Status | Meaning |
|---|---|
401 |
Header missing or token invalid |
Check whether the modem is connected and responsive. No authentication required.
curl http://localhost:8000/healthResponse 200:
{
"status": "ok",
"port": "/dev/ttyUSB2",
"message": "Modem responding to AT commands"
}status is "ok" or "error".
Return IMSI, SIM card ID, signal strength, and network registration state.
curl -H "X-API-Key: $KEY" http://localhost:8000/statusResponse 200:
{
"imsi": "238010123456789",
"ccid": "8945110000000000001",
"signal_strength": 18,
"signal_dbm": -75.0,
"network_registration": "1",
"network_registration_text": "Registered (home network)",
"smsc": "+4540590000",
"modem_port": "/dev/ttyUSB2"
}signal_dbm is null if the modem reports no signal (99).
Check whether the SIM is unlocked, waiting for a PIN, or PUK-locked.
curl -H "X-API-Key: $KEY" http://localhost:8000/sim/pinResponse 200:
{
"state": "READY",
"description": "SIM is unlocked and ready"
}Possible state values:
| Value | Meaning |
|---|---|
READY |
SIM unlocked and ready |
SIM PIN |
PIN required |
SIM PUK |
PUK-locked — contact your carrier |
Submit the SIM PIN to unlock the card.
Only needed if SIM_PIN was not set in .env (automatic unlock on startup).
curl -X POST http://localhost:8000/sim/pin \
-H "X-API-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{"pin": "1234"}'Request body:
{
"pin": "1234"
}Response 200 — the new PIN state after entry:
{
"state": "READY",
"description": "SIM is unlocked and ready"
}Response 400 — wrong PIN or modem error.
Send an SMS message to one or more recipients.
# Single recipient
curl -X POST http://localhost:8000/sms/send \
-H "X-API-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{"to": "+4512345678", "message": "Hello!"}'
# Multiple recipients (JSON array)
curl -X POST http://localhost:8000/sms/send \
-H "X-API-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{"to": ["+4512345678", "+4687654321"], "message": "Hello all!"}'Request body:
{
"to": "+4512345678",
"message": "Hello from the gateway!"
}| Field | Type | Description |
|---|---|---|
to |
string | array | Recipient(s) in international format. Accepts: "+4512345678", "+4512345678,+4687654321", or ["+4512345678", "+4687654321"] |
message |
string | Message text. Full Unicode supported (ÆØÅæøå, emoji). Long messages are auto-split at 70 chars/segment (UCS-2) |
Response 202:
{
"ok": true,
"results": [
{
"to": "+4512345678",
"ok": true,
"message_reference": 42,
"error": null
}
]
}Top-level ok is true only if all recipients succeeded. Each entry in results contains:
| Field | Description |
|---|---|
to |
The recipient number |
ok |
Whether this individual send succeeded |
message_reference |
Modem-assigned delivery reference (null if unavailable) |
error |
Error message if ok is false, otherwise null |
List SMS messages stored on the SIM card.
# All messages
curl -H "X-API-Key: $KEY" http://localhost:8000/sms
# Unread messages only
curl -H "X-API-Key: $KEY" "http://localhost:8000/sms?status=REC%20UNREAD"Query parameters:
| Parameter | Default | Description |
|---|---|---|
status |
(all) | Filter by status: REC UNREAD, REC READ, STO UNSENT, STO SENT. Omit for all. |
Response 200:
{
"messages": [
{
"index": 1,
"status": "REC UNREAD",
"sender": "+4512345678",
"timestamp": "26/05/07,10:23:15+08",
"message": "Hello!"
}
],
"count": 1
}index is the SIM storage slot (1-based). Use it with the read and delete endpoints.
Read a single SMS message by SIM storage index.
curl -H "X-API-Key: $KEY" http://localhost:8000/sms/1Path parameter: index — integer ≥ 1.
Response 200:
{
"index": 1,
"status": "REC READ",
"sender": "+4512345678",
"timestamp": "26/05/07,10:23:15+08",
"message": "Hello!"
}Response 404 — no message at that index.
Delete a single SMS message by index.
curl -X DELETE -H "X-API-Key: $KEY" http://localhost:8000/sms/1Response 200:
{
"ok": true,
"deleted": "index 1"
}Response 404 — no message at that index.
Delete all SMS messages from SIM storage.
curl -X DELETE -H "X-API-Key: $KEY" http://localhost:8000/smsResponse 200:
{
"ok": true,
"deleted": "all"
}Open a persistent Server-Sent Events (SSE) connection. Each incoming SMS is pushed
as a JSON data event in real time.
Requires MONITOR_PORT to be configured (default /dev/ttyUSB7). The monitor listens
for +CMTI URCs on the secondary AT port and fetches the full message via the main port.
curl -N -H "X-API-Key: $KEY" http://localhost:8000/sms/streamEvent format:
data: {"index": 3, "sender": "+4512345678", "message": "Hello!", "timestamp": "26/05/08,10:30:00+08", "status": "REC UNREAD"}
: keepalive
A : keepalive comment is sent every 15 seconds to prevent the connection from timing out.
| Event field | Description |
|---|---|
index |
SIM storage index of the received message |
sender |
Sender phone number |
message |
Decoded message text |
timestamp |
Modem timestamp string (YY/MM/DD,HH:MM:SS+tz) |
status |
Always "REC UNREAD" for freshly received messages |
Response 503 — MONITOR_PORT is not configured or monitor failed to start.
Return all configured token names and their aggregated usage statistics.
Token values are never exposed — only names.
curl -H "X-API-Key: $KEY" http://localhost:8000/tokensResponse 200:
{
"auth_enabled": true,
"tokens": [
{
"name": "admin",
"request_count": 142,
"sms_sent": 37,
"last_used": "2026-05-07T09:14:22Z"
},
{
"name": "homeassistant",
"request_count": 511,
"sms_sent": 204,
"last_used": "2026-05-07T11:00:01Z"
}
]
}auth_enabled is false when no tokens are configured (open access mode).
last_used is null if the token has never been used.
Query the full request log with optional filters. Results are newest-first.
# Last 100 entries
curl -H "X-API-Key: $KEY" http://localhost:8000/logs
# SMS sends by a specific token in a date range
curl -H "X-API-Key: $KEY" \
"http://localhost:8000/logs?token=homeassistant&endpoint=/sms/send&since=2026-05-01T00:00:00Z&limit=50"Query parameters:
| Parameter | Default | Description |
|---|---|---|
limit |
100 |
Max entries to return (1–1000) |
offset |
0 |
Entries to skip (pagination) |
token |
(all) | Filter by token name |
since |
(all) | ISO-8601 start timestamp, e.g. 2026-05-01T00:00:00Z |
until |
(all) | ISO-8601 end timestamp, e.g. 2026-05-07T23:59:59Z |
recipient |
(all) | Filter by recipient number (SMS sends only) |
endpoint |
(all) | Filter by endpoint path, e.g. /sms/send |
All filters are combinable.
Response 200:
{
"entries": [
{
"id": 1024,
"timestamp": "2026-05-07T11:00:01Z",
"token_name": "homeassistant",
"method": "POST",
"endpoint": "/sms/send",
"status_code": 202,
"recipient": "+4512345678"
}
],
"total": 1024,
"limit": 100,
"offset": 0
}recipient is only populated for POST /sms/send requests, null for all others.
total reflects the count matching your filters, not the page size.
The SIM7600E has a built-in GNSS module. It must be started explicitly (or via GPS_AUTOSTART=1)
and requires a clear sky view for a fix.
Return whether the GPS module is running.
curl -H "X-API-Key: $KEY" http://localhost:8000/gps/statusResponse 200:
{
"running": true,
"streaming": false
}Power on the GNSS module in standalone mode.
Allow 30–60 seconds outdoors for a first fix after starting.
curl -X POST -H "X-API-Key: $KEY" http://localhost:8000/gps/startResponse 200: same shape as GET /gps/status.
Response 503 — modem error.
Power off the GNSS module to save battery/power.
curl -X POST -H "X-API-Key: $KEY" http://localhost:8000/gps/stopResponse 200: same shape as GET /gps/status.
Return the current position from the GNSS module.
curl -H "X-API-Key: $KEY" http://localhost:8000/gps/locationResponse 200 (fix acquired):
{
"fix": true,
"latitude": 55.6761,
"longitude": 12.5683,
"altitude_m": 14.2,
"speed_kmh": 0.0,
"course_deg": 0.0,
"hdop": 1.2,
"satellites_in_view": 8,
"utc_datetime": "2026-05-07T11:00:00Z"
}Response 200 (no fix yet):
{
"fix": false,
"latitude": null,
"longitude": null,
"altitude_m": null,
"speed_kmh": null,
"course_deg": null,
"hdop": null,
"satellites_in_view": null,
"utc_datetime": null
}Response 503 — GPS module is not running.
These endpoints give direct access to the modem. Treat them as privileged — restrict access to trusted operators.
Send a raw AT command to the modem and return the response.
curl -H "X-API-Key: $KEY" "http://localhost:8000/debug/at?cmd=AT%2BCSQ"Query parameter: cmd — AT command to send, e.g. AT+CSQ.
Response 200:
{
"command": "AT+CSQ",
"response": "+CSQ: 18,0\r\n\r\nOK",
"ok": true
}ok is false (not an HTTP error) if the modem returns an error response.
Probe /dev/ttyUSB0–7 and report which ports respond to AT commands.
Useful for identifying the correct MODEM_PORT value.
curl -H "X-API-Key: $KEY" http://localhost:8000/debug/portsResponse 200:
{
"ports": [
{ "port": "/dev/ttyUSB0", "status": "no_response", "response": null, "error": null },
{ "port": "/dev/ttyUSB1", "status": "no_response", "response": null, "error": null },
{ "port": "/dev/ttyUSB2", "status": "responsive", "response": "\r\nOK\r\n", "error": null },
{ "port": "/dev/ttyUSB3", "status": "error", "response": null, "error": "Permission denied" }
]
}status values: responsive, no_response, error.
Ports not passed through to the Docker container will show "error".
All errors return JSON:
{
"error": "Internal server error",
"detail": "Serial timeout on /dev/ttyUSB2"
}Common HTTP status codes:
| Status | Meaning |
|---|---|
400 |
Bad request (e.g. wrong PIN) |
401 |
Missing or invalid X-API-Key |
404 |
Resource not found (e.g. SMS index does not exist) |
422 |
Validation error (missing or malformed request body field) |
502 |
Modem failed to complete the operation |
503 |
Modem unavailable or GPS not running |
500 |
Unexpected internal error |
import httpx
BASE = "http://localhost:8000"
KEY = "mysecrettoken"
HEADERS = {"X-API-Key": KEY}
# Health check (no auth)
r = httpx.get(f"{BASE}/health")
print(r.json()) # {"status": "ok", ...}
# Send an SMS
r = httpx.post(
f"{BASE}/sms/send",
headers=HEADERS,
json={"to": "+4512345678", "message": "Hello!"},
)
print(r.json()) # {"ok": true, "message_reference": 42}
# List unread SMS
r = httpx.get(f"{BASE}/sms", headers=HEADERS, params={"status": "REC UNREAD"})
print(r.json())All settings live in docker/.env (copy from docker/.env.example).
| Variable | Default | Description |
|---|---|---|
MODEM_PORT |
/dev/ttyUSB2 |
Serial device for AT commands |
MONITOR_PORT |
/dev/ttyUSB7 |
Secondary AT port for incoming SMS monitoring (GET /sms/stream) |
BAUD_RATE |
115200 |
Serial baud rate |
CMD_TIMEOUT |
5.0 |
Seconds to wait per AT command. Increase to 30.0 for slow carriers |
API_PORT |
8000 |
Port for the full API |
RESTRICTED_PORT |
8007 |
Port for the restricted API (health, status, send only) |
API_KEYS |
(empty) | Named tokens: name:token,name:token |
API_KEY |
(empty) | Legacy single token (name "default") |
SIM_PIN |
(empty) | SIM PIN for automatic unlock on startup |
GPS_AUTOSTART |
0 |
Set to 1 to start GPS on daemon startup |
LOG_DB |
/data/sms-gateway.db |
Path to the SQLite request log (inside container) |
DEBUG |
0 |
Set to 1 to log raw AT I/O to stdout |