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)¶
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)¶
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)¶
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¶
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"
}
Online Sessions on NAS¶
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¶
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¶
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:
WhatsApp API¶
Outbox (Message History)¶
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)¶
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¶
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¶
Simple health check for uptime monitoring. No authentication required.
Response:
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¶
Renders the portal login page (Inertia: Portal/Login).
Login¶
Request Body:
Response (success): Redirects to /portal/dashboard
Response (failure): Returns validation errors: { "errors": { "username": ["Invalid credentials."] } }
Logout¶
Destroys the portal session. Redirects to /portal/login.
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¶
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¶
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¶
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¶
Guard: auth:portal
Inertia Component: Portal/Profile
Returns the customer's editable contact information.
Portal Profile Update¶
Guard: auth:portal
Request Body:
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)¶
Forbidden (403)¶
Not Found (404)¶
Server Error (500)¶
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-Afterheader
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).