Documentation

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

MethodPathAuthPurpose
GET/healthnoneLiveness probe
GET/api/host/infoBearerPhone number, carrier, SIMs, counters
GET/api/diagnosticsBearerFull health snapshot for a headless gateway
POST/api/sms/sendBearerSend an SMS
GET/api/messagesBearerPaginated send history
GET/api/messages/:idBearerOne message’s status
GET/api/statsBearerToday / 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 returns 429 with a Retry-After header and retryAfterSeconds.
  • Per-caller burst: roughly 10 requests per second (with a small burst allowance) on every endpoint, including /health. Over that returns 429.

Next