Skip to content

09 — API Documentation

System: PyroRadius ISP Billing & RADIUS Management
Stack: Laravel 13 · Inertia.js v2 · React 18 · PostgreSQL 16
Last Updated: 2026-07-13


Overview: Two API Layers

PyroRadius exposes data through two distinct mechanisms:

1. Inertia Page Responses (Primary — routes/web.php)

For every full page (customer list, invoice view, dashboard), the controller calls Inertia::render('Page/Name', $props). The React frontend never calls these URLs with a raw fetch() — Inertia handles navigation automatically. No explicit "API design" is needed for page data because the controller just passes what the page needs as props.

When you navigate to /customers, the server returns:

{
  "component": "Customers/Index",
  "props": {
    "customers": { "data": [...], "links": {...}, "meta": {...} },
    "filters": { "search": "", "status": "active" },
    "auth": { "user": {...} },
    "flash": { "success": null, "error": null }
  },
  "url": "/customers",
  "version": "abc123"
}

2. AJAX/API Endpoints (Secondary — routes/api.php)

Used for real-time data that changes without navigation: live traffic graphs, online customer count, typeahead search, status polls. These endpoints return JSON and are called from React components with Axios.


Authentication for API Calls

All API routes use Laravel Sanctum stateful authentication — the same session cookie that authenticated the web application. No bearer token is needed for internal frontend use.

Every Axios request from the frontend automatically includes:

Cookie: laravel_session=...
X-XSRF-TOKEN: ...        ← CSRF token from cookie, set by bootstrap.js
X-Requested-With: XMLHttpRequest
Accept: application/json

If the session has expired, the API returns 401 Unauthenticated and the Axios interceptor in bootstrap.js redirects to /login.


API Endpoints Reference

Customer API

Search Customers (Typeahead)

GET /api/customers

Used by the recharge modal and assignment dropdowns to search customers by name, username, or phone.

Query Parameters:

Parameter Type Required Description
q string Yes Search term (min 2 chars)
status string No Filter by status: active, expired, suspended, all
limit integer No Max results (default: 10, max: 50)

Permission Required: view_customers

Response:

[
  {
    "id": 42,
    "name": "Ahmad Khan",
    "username": "ahmad.khan",
    "phone": "03001234567",
    "status": "active",
    "package_name": "10 Mbps Basic",
    "expiry_date": "2026-07-31",
    "area": "Gulberg"
  }
]

Filter Customers (Table)

GET /api/customers/filter

Used by the Customers/Index page for server-side filtering without a full Inertia navigation (e.g. changing a filter dropdown without resetting scroll position).

Query Parameters:

Parameter Type Description
search string Name / username / phone
status string active, expired, suspended, disconnected, all
package_id integer Filter by package
nas_id integer Filter by NAS
area string Filter by area
expiry_from date ISO date string
expiry_to date ISO date string
page integer Page number (default: 1)
per_page integer Items per page (default: 15, max: 100)
sort string Column to sort by
direction string asc or desc

Permission Required: view_customers

Response:

{
  "data": [
    {
      "id": 42,
      "name": "Ahmad Khan",
      "username": "ahmad.khan",
      "status": "active",
      "package_name": "10 Mbps Basic",
      "expiry_date": "2026-07-31",
      "outstanding_balance": 0,
      "nas_name": "NAS-01",
      "area": "Gulberg"
    }
  ],
  "meta": {
    "current_page": 1,
    "total": 1240,
    "per_page": 15,
    "last_page": 83
  }
}

Customer Ledger (AJAX)

GET /api/customers/{customer}/ledger

Returns ledger entries for a customer in reverse-chronological order. Used by the ledger page and customer show page sidebar widget.

Path Parameters:

Parameter Type Description
customer integer Customer ID

Query Parameters:

Parameter Type Description
from date Filter from date
to date Filter to date
type string debit, credit, or all
page integer Page number

Permission Required: view_customers

Response:

{
  "data": [
    {
      "id": 200,
      "type": "credit",
      "amount": "1500.00",
      "description": "Payment received — Cash",
      "reference": "RCP202506-00043",
      "running_balance": "0.00",
      "created_at": "2026-06-15T10:30:00Z",
      "created_by": "Admin User"
    },
    {
      "id": 199,
      "type": "debit",
      "amount": "1500.00",
      "description": "Invoice INV202506-00038 generated",
      "reference": "INV202506-00038",
      "running_balance": "1500.00",
      "created_at": "2026-06-01T02:00:00Z",
      "created_by": "System"
    }
  ],
  "meta": {
    "current_page": 1,
    "total": 48,
    "per_page": 20,
    "current_balance": "0.00"
  }
}

NAS API

Live Traffic Data

GET /api/nas/{nas}/traffic

Returns real-time interface traffic for a NAS device. Tries MikroTik RouterOS API first (port 8728); falls back to SNMP IF-MIB if the API call fails or times out.

Path Parameters:

Parameter Type Description
nas integer NAS device ID

Query Parameters:

Parameter Type Description
interface string Interface name filter (optional)
resolution string 1m, 5m, 1h — time resolution for history (default: 5m)
points integer Number of history data points (default: 60, max: 288)

Permission Required: view_nas

Response (success — MikroTik):

{
  "source": "mikrotik_api",
  "nas_id": 5,
  "nas_name": "NAS-GULBERG",
  "interfaces": [
    {
      "name": "ether1",
      "rx_bps": 45234560,
      "tx_bps": 12874320,
      "rx_packets": 34120,
      "tx_packets": 22450,
      "rx_errors": 0,
      "tx_errors": 0
    }
  ],
  "history": [
    { "time": "2026-07-13T10:55:00Z", "rx": 44200000, "tx": 12500000 },
    { "time": "2026-07-13T11:00:00Z", "rx": 45234560, "tx": 12874320 }
  ],
  "fetched_at": "2026-07-13T11:00:30Z"
}

Response (fallback — SNMP):

{
  "source": "snmp_fallback",
  "nas_id": 5,
  "nas_name": "NAS-GULBERG",
  "interfaces": [...],
  "history": [...],
  "snmp_error": null,
  "fetched_at": "2026-07-13T11:00:31Z"
}

Error Response:

{
  "error": "Both MikroTik API and SNMP unreachable",
  "nas_id": 5,
  "fetched_at": "2026-07-13T11:00:35Z"
}
HTTP 503


Online Sessions on NAS

GET /api/nas/{nas}/online

Returns currently active RADIUS sessions for a NAS device, read from radacct.

Path Parameters:

Parameter Type Description
nas integer NAS device ID

Permission Required: view_nas

Response:

{
  "nas_id": 5,
  "session_count": 38,
  "sessions": [
    {
      "radacctid": 10042,
      "username": "ahmad.khan",
      "customer_name": "Ahmad Khan",
      "framedipaddress": "10.10.1.55",
      "callingstationid": "00:0C:42:AA:BB:CC",
      "acctstarttime": "2026-07-13T08:22:00Z",
      "duration_minutes": 158,
      "input_octets": 230458912,
      "output_octets": 98234560
    }
  ],
  "fetched_at": "2026-07-13T11:01:00Z"
}

Dashboard API

Online Count

GET /api/dashboard/online

Returns the current total count of online customers across all NAS devices. The result is cached in Redis for 30 seconds. This endpoint is polled by the Dashboard every 30 seconds.

Permission Required: view_dashboard

Response:

{
  "count": 412,
  "breakdown": [
    { "nas_id": 5, "nas_name": "NAS-GULBERG", "online": 180 },
    { "nas_id": 6, "nas_name": "NAS-DHA",     "online": 232 }
  ],
  "cached": true,
  "cache_age_seconds": 12,
  "fetched_at": "2026-07-13T11:05:30Z"
}

Package API

Package Availability

GET /api/packages/{package}/availability

Returns whether a package has available capacity and how many customers are currently on it.

Path Parameters:

Parameter Type Description
package integer Package ID

Permission Required: view_packages

Response:

{
  "package_id": 3,
  "package_name": "20 Mbps Premium",
  "active_customers": 145,
  "max_customers": 200,
  "available": true,
  "available_slots": 55
}

If max_customers is null, the package has no capacity limit:

{
  "package_id": 3,
  "available": true,
  "available_slots": null,
  "max_customers": null
}

WhatsApp API

Outbox (Message History)

GET /api/whatsapp/outbox

Returns sent and pending WhatsApp messages, used for the outbox polling view.

Query Parameters:

Parameter Type Description
status string pending, sent, failed, all (default: all)
customer_id integer Filter by customer
from date Date filter
to date Date filter
page integer Page number

Permission Required: view_whatsapp

Response:

{
  "data": [
    {
      "id": 8841,
      "customer_id": 42,
      "customer_name": "Ahmad Khan",
      "phone": "923001234567",
      "message_type": "payment_received",
      "body": "Dear Ahmad, your payment of Rs. 1,500 has been received...",
      "status": "sent",
      "sent_at": "2026-07-13T10:30:05Z",
      "error": null
    },
    {
      "id": 8840,
      "customer_id": 99,
      "customer_name": "Sara Malik",
      "phone": "923339876543",
      "message_type": "expiry_reminder",
      "body": "Dear Sara, your package expires in 3 days...",
      "status": "failed",
      "sent_at": null,
      "error": "HTTP 400 from WhatsApp API: invalid phone number"
    }
  ],
  "meta": {
    "current_page": 1,
    "total": 4220,
    "per_page": 20
  }
}

Invoice API

Invoice Search (Typeahead)

GET /api/invoices

Used in the recharge modal to let the user select a specific invoice to apply a payment against.

Query Parameters:

Parameter Type Description
customer_id integer Required. Filter to this customer's invoices
status string unpaid, partial, paid, all (default: unpaid)

Permission Required: view_billing

Response:

[
  {
    "id": 38,
    "invoice_no": "INV202506-00038",
    "amount": 1500.00,
    "paid": 0.00,
    "balance": 1500.00,
    "status": "unpaid",
    "period_start": "2026-06-01",
    "period_end": "2026-06-30",
    "due_date": "2026-06-05"
  }
]

RADIUS Status API

Customer RADIUS Status

GET /api/radius/{username}/status

Returns the current state of a customer's RADIUS entries (whether they have entries in radcheck, radusergroup, and radgroupreply).

Path Parameters:

Parameter Type Description
username string RADIUS username (URL-encoded if needed)

Permission Required: view_nas

Response:

{
  "username": "ahmad.khan",
  "has_radcheck": true,
  "has_radusergroup": true,
  "group_name": "pkg_10mbps_basic",
  "has_radgroupreply": true,
  "speed_attributes": [
    { "attribute": "Mikrotik-Rate-Limit", "value": "10M/5M" }
  ],
  "active_sessions": 1,
  "last_auth": "2026-07-13T08:22:00Z"
}

Health API

Ping

GET /api/health/ping

Simple health check for uptime monitoring. No authentication required.

Response:

{ "status": "ok", "timestamp": "2026-07-13T11:10:00Z" }

Customer Portal API (routes/portal.php)

The customer portal has its own auth guard (portal) and its own set of routes. Customers authenticate with their username + password or a signed one-time URL generated by an admin. The portal exposes only the data belonging to the authenticated customer — no cross-customer data access is possible.

Portal Authentication

Show Login

GET /portal/login

Renders the portal login page (Inertia: Portal/Login).


Login

POST /portal/login

Request Body:

{
  "username": "ahmad.khan",
  "password": "mypassword"
}

Response (success): Redirects to /portal/dashboard

Response (failure): Returns validation errors: { "errors": { "username": ["Invalid credentials."] } }


Logout

POST /portal/logout

Destroys the portal session. Redirects to /portal/login.


Portal Dashboard

GET /portal/dashboard

Guard: auth:portal
Inertia Component: Portal/Dashboard

Props Returned:

{
  "customer": {
    "name": "Ahmad Khan",
    "username": "ahmad.khan",
    "package_name": "10 Mbps Basic",
    "status": "active",
    "expiry_date": "2026-07-31",
    "outstanding_balance": 0
  },
  "recent_invoices": [...],
  "recent_payments": [...]
}

Portal Invoices

GET /portal/invoices

Guard: auth:portal
Inertia Component: Portal/Invoices

Returns only invoices belonging to the authenticated customer. Supports pagination (15 per page).

Props:

{
  "invoices": {
    "data": [
      {
        "invoice_no": "INV202506-00038",
        "amount": 1500.00,
        "paid": 1500.00,
        "status": "paid",
        "period_start": "2026-06-01",
        "period_end": "2026-06-30",
        "pdf_url": "/portal/invoices/38/pdf"
      }
    ],
    "meta": { "current_page": 1, "total": 12, "per_page": 15 }
  }
}

Portal Invoice PDF Download

GET /portal/invoices/{invoice}/pdf

Guard: auth:portal

Returns a PDF file download. Validates that the invoice belongs to the authenticated customer before serving. Returns 404 if the invoice belongs to a different customer.


Portal Payment History

GET /portal/payments

Guard: auth:portal
Inertia Component: Portal/Payments

Returns the payment history for the authenticated customer.

Props:

{
  "payments": {
    "data": [
      {
        "receipt_no": "RCP202506-00043",
        "amount": 1500.00,
        "method": "cash",
        "created_at": "2026-06-15T10:30:00Z",
        "receipt_url": "/portal/payments/99/receipt"
      }
    ],
    "meta": { "current_page": 1, "total": 12, "per_page": 15 }
  }
}

Portal Profile

GET /portal/profile

Guard: auth:portal
Inertia Component: Portal/Profile

Returns the customer's editable contact information.


Portal Profile Update

PUT /portal/profile

Guard: auth:portal

Request Body:

{
  "phone": "03001234567",
  "email": "ahmad@example.com",
  "address": "House 5, Block A, Gulberg"
}

The customer can update their phone, email, and address. They cannot change their username, package, or billing details.

Response (success): Redirects to /portal/profile with flash success.


Error Response Format

All API endpoints return errors in a consistent format:

Validation Error (422)

{
  "message": "The given data was invalid.",
  "errors": {
    "amount": ["The amount field is required."],
    "method": ["The selected method is invalid."]
  }
}

Unauthorised (401)

{ "message": "Unauthenticated." }

Forbidden (403)

{ "message": "This action is unauthorized." }

Not Found (404)

{ "message": "No query results for model [App\\Models\\Customer] 9999." }

Server Error (500)

{
  "message": "Server Error",
  "reference": "ERR-20260713-a1b2c3"
}

Rate Limiting

API routes are subject to Laravel's default throttle:api rate limiter:

  • 60 requests per minute per authenticated user
  • Rate limit headers are returned: X-RateLimit-Limit, X-RateLimit-Remaining
  • Exceeded limit returns HTTP 429 with Retry-After header

Real-time polling endpoints (traffic, online count) are designed to not exceed this limit when used at their default poll intervals (30s for online count, chart refresh on demand).