REST API
Hiras runs a small HTTP server on the phone. Every device on your local network talks to it the same way — a JSON request in, a JSON response out.
Base URL and port
The server binds to port 8080 and is reachable at your phone’s LAN address:
http://<phone-ip>:8080
The address is shown in the app once you tap Start. The port is fixed in this version.
Authentication
Every endpoint except GET /health requires a bearer token — the sk_local_ key shown in
the app:
Authorization: Bearer sk_local_xxxxxxxxxxxxxxxxxxxxxxxx
See Authentication for details.
Local-network guard
Before routing, Hiras checks the caller’s address. Requests from outside your private network (loopback, RFC 1918, carrier-grade NAT, and link-local ranges are allowed) are rejected:
{ "error": "Forbidden: local network access only" }
This runs before authentication, so a non-local caller never even reaches the key check.
Endpoints
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET | /health | none | Liveness probe |
GET | /api/host/info | Bearer | Phone number, carrier, SIMs, counters |
GET | /api/diagnostics | Bearer | Full health snapshot for a headless gateway |
POST | /api/sms/send | Bearer | Send an SMS |
GET | /api/messages | Bearer | Paginated send history |
GET | /api/messages/:id | Bearer | One message’s status |
GET | /api/stats | Bearer | Today / month roll-ups |
GET /health
The only unauthenticated route (still local-network guarded). Deliberately minimal:
{
"status": "running",
"version": "1.0.0",
"smsEnabled": true,
"mode": "real",
"simulation": "none",
"testMode": false
}
POST /api/sms/send
Send a message. The request body is capped at 16 KB.
{ "to": "09171234567", "message": "Hello", "simSlot": 2 }
simSlot is optional and 1-based; omit it to use the default SIM. On success:
{
"success": true,
"mode": "real",
"smsSent": true,
"messageId": "msg_...",
"status": "queued",
"segments": 1,
"simSlot": 1
}
A message longer than one segment returns a warning describing the split. A failed send
returns 502 with a structured error (see below).
GET /api/messages
Paginated history, newest first. Query parameters: limit, offset, status.
{
"success": true,
"data": [
{
"id": "msg_...",
"to": "09171234567",
"body": "Hello",
"segments": 1,
"billedSegments": 1,
"status": "sent",
"reason": null,
"sourceIp": "192.168.1.20",
"createdAt": "2026-07-02T15:00:00Z",
"updatedAt": "2026-07-02T15:00:03Z"
}
],
"pagination": { "limit": 20, "offset": 0, "total": 1, "hasNext": false }
}
History keeps the newest 1000 messages on the device and is never uploaded.
Response and error shapes
Read endpoints wrap their payload as { "success": true, "data": ... }. The send path and
status routes return a structured error on failure:
{
"success": false,
"smsSent": false,
"error": { "code": "send_failed", "message": "..." }
}
Some guard responses use a simpler { "error": "..." } shape — for example a 401
Unauthorized, or a 403 from the local-network guard.
Rate limits
Two independent guards protect the gateway. Both are fixed in this version:
- Send path: 60 SMS segments per minute, plus a minimum 1 second between requests. A
message that could never fit returns
400 message_too_long; a temporary overage returns429with aRetry-Afterheader andretryAfterSeconds. - Per-caller burst: roughly 10 requests per second (with a small burst allowance) on
every endpoint, including
/health. Over that returns429.
Next
- Authentication — the API key in full.
- Getting started — the four-step setup.