PyroRadius Mobile API Documentation¶
Document: PHASE2_10_Mobile_API.md Version: 2.0 Date: 2026-07-13 Author: PyroNet Solutions
Overview¶
The PyroRadius Mobile API provides a RESTful JSON interface for mobile applications and external integrations. It is completely separate from the web (Inertia/session-based) interface and uses Laravel Sanctum token authentication instead of session cookies.
Base URL: https://pyroradius.pyronet.com.pk/api/mobile
Content-Type: All requests must include Content-Type: application/json. All responses are JSON.
Version: The API is currently unversioned. Breaking changes will be announced with sufficient notice.
Authentication¶
The Mobile API uses Sanctum token-based authentication (Personal Access Tokens). Unlike the web interface (which uses session cookies), every API request must include a Bearer token in the Authorization header.
Tokens are issued at login and must be stored securely by the client. Tokens do not expire automatically but can be revoked via logout.
All permissions are still fully enforced: a dealer user with a valid token can only access their own customers, just as in the web interface.
Rate Limits¶
| Endpoint Group | Limit | Window |
|---|---|---|
| POST /login | 5 requests | per minute |
| POST /customers/{id}/recharge | 10 requests | per minute |
| All other endpoints | Laravel default (60) | per minute |
When a rate limit is exceeded, the API returns HTTP 429 Too Many Requests with a Retry-After header.
Response Format Standards¶
Success — Single Resource¶
Success — Paginated List¶
{
"data": [
{ "id": 1, "field": "value" },
{ "id": 2, "field": "value" }
],
"meta": {
"current_page": 1,
"per_page": 20,
"total": 150,
"last_page": 8
}
}
Error Response¶
{
"message": "Human-readable error description",
"errors": {
"field_name": ["Validation error message"]
}
}
The errors key is only present on 422 Unprocessable Entity responses.
HTTP Status Codes¶
| Code | Meaning |
|---|---|
| 200 | OK — Request succeeded |
| 201 | Created — Resource created successfully |
| 204 | No Content — Success with no body (e.g., logout) |
| 401 | Unauthenticated — Token missing or invalid |
| 403 | Forbidden — Authenticated but lacks permission |
| 404 | Not Found — Resource does not exist |
| 422 | Unprocessable Entity — Validation failed |
| 429 | Too Many Requests — Rate limit exceeded |
| 500 | Server Error — Internal error |
Endpoints¶
AUTH¶
LOGIN¶
Method: POST
URL: /api/mobile/login
Auth: Public (no token required)
Rate Limit: 5 requests/minute
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
| string | Yes | User's email address | |
| password | string | Yes | User's password |
| device_name | string | No | Device identifier for token labeling (default: "mobile") |
Example Request:
{
"email": "admin@pyronet.com.pk",
"password": "change-me-on-first-login",
"device_name": "Samsung Galaxy S24"
}
Response (200):
{
"token": "1|abcdefghijklmnopqrstuvwxyz1234567890",
"user": {
"id": 1,
"name": "Admin User",
"email": "admin@pyronet.com.pk",
"role": "super_admin",
"permissions": [
"view_customers",
"create_customers",
"recharge_customers",
"view_reports"
],
"dealer_id": null
}
}
Error Responses:
422: Email or password incorrect429: Too many login attempts
Notes:
Store the token value securely (e.g., in device secure storage). Include it in the Authorization: Bearer {token} header for all subsequent requests. The permissions array tells the app which UI features to enable/disable.
GET CURRENT USER¶
Method: GET
URL: /api/mobile/me
Auth: Required (Bearer token)
Rate Limit: Default (60/min)
Request Body: None
Response (200):
{
"data": {
"id": 1,
"name": "Admin User",
"email": "admin@pyronet.com.pk",
"role": "super_admin",
"permissions": [
"view_customers",
"create_customers",
"recharge_customers",
"edit_customers",
"delete_customers",
"view_reports",
"view_financials",
"manage_dealers",
"manage_packages",
"manage_nas",
"view_network",
"send_whatsapp"
],
"dealer_id": null,
"created_at": "2025-01-15T10:00:00Z"
}
}
Error Responses:
401: Token invalid or not provided
Notes:
Use this endpoint to refresh user permissions without re-logging in. Also useful after an admin changes a user's permissions — the app can re-fetch to get updated permissions.
LOGOUT¶
Method: POST
URL: /api/mobile/logout
Auth: Required (Bearer token)
Rate Limit: Default (60/min)
Request Body: None
Response (200):
Error Responses:
401: Token already invalid
Notes:
This revokes the current access token on the server. The client should delete the locally stored token after receiving a success response. To log out all devices, call this endpoint from each device separately (each token is independent).
REFRESH TOKEN¶
Method: POST
URL: /api/mobile/refresh
Auth: Required (Bearer token)
Rate Limit: Default (60/min)
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
| device_name | string | No | Device name for the new token |
Response (200):
Error Responses:
401: Current token is invalid
Notes:
Revokes the current token and issues a new one. The old token becomes invalid immediately. The client must replace its stored token with the new value. Useful for rotating tokens periodically for security.
DASHBOARD¶
GET DASHBOARD STATS¶
Method: GET
URL: /api/mobile/dashboard
Auth: Required (Bearer token)
Rate Limit: Default (60/min)
Request Parameters: None
Response (200):
{
"data": {
"total_customers": 1247,
"active_customers": 1089,
"expired_customers": 98,
"suspended_customers": 43,
"disabled_customers": 17,
"online_now": 312,
"monthly_revenue": 2847500.00,
"today_revenue": 125000.00,
"pending_tickets": 8,
"monthly_collections": {
"current_month": 2847500.00,
"last_month": 2634000.00,
"growth_percent": 8.1
},
"recent_recharged": [
{
"id": 501,
"name": "Muhammad Ali",
"pppoe_username": "ali_01",
"amount": 2500,
"recharged_at": "2026-07-13T14:30:00Z"
}
]
}
}
Error Responses:
401: Unauthenticated403: Insufficient permissions
Notes:
For dealer users, all stats are scoped to their own customers only. online_now is fetched via RadiusService::getOnlineCount() which queries the radacct table for active sessions. monthly_revenue counts all payments in the current calendar month.
CUSTOMERS¶
LIST CUSTOMERS¶
Method: GET
URL: /api/mobile/customers
Auth: Required (Bearer token)
Rate Limit: Default (60/min)
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| page | integer | No | Page number (default: 1) |
| per_page | integer | No | Results per page (default: 20, max: 100) |
| status | string | No | Filter by status: active, expired, suspended, disabled, temp_extended |
| search | string | No | Search by name, username, mobile |
| dealer_id | integer | No | Filter by dealer (super_admin only) |
| package_id | integer | No | Filter by package |
| area_id | integer | No | Filter by area |
| expiring_days | integer | No | Show customers expiring within N days |
Response (200):
{
"data": [
{
"id": 101,
"name": "Muhammad Ali",
"pppoe_username": "ali_01",
"mobile": "03001234567",
"status": "active",
"expiry_date": "2026-08-13",
"package_name": "10 Mbps Unlimited",
"package_price": 2500,
"dealer_name": "City Zone Dealer",
"area_name": "Johar Town",
"online": true,
"days_remaining": 31
}
],
"meta": {
"current_page": 1,
"per_page": 20,
"total": 1247,
"last_page": 63
}
}
Error Responses:
401: Unauthenticated403: Forbidden
Notes:
Dealer users can only see their own customers (dealer_id = auth()->id()). The online field is a boolean from RadiusService::isOnline($pppoe_username). For large lists, avoid requesting online status as it adds latency — it is included only if the backend computes it efficiently via cached data.
CREATE CUSTOMER¶
Method: POST
URL: /api/mobile/customers
Auth: Required (Bearer token) — requires create_customers permission
Rate Limit: Default (60/min)
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Customer full name |
| pppoe_username | string | Yes | PPPoE username (unique, alphanumeric + underscore) |
| pppoe_password | string | Yes | PPPoE password (min 8 chars) |
| mobile | string | Yes | Mobile number (Pakistani format preferred) |
| string | No | Email address | |
| address | string | No | Physical address |
| cnic | string | No | CNIC number |
| package_id | integer | Yes | Package ID from /api/mobile/packages |
| dealer_id | integer | No | Dealer ID (super_admin can assign; dealers auto-assigned) |
| area_id | integer | No | Area ID |
| nas_id | integer | No | NAS/router ID |
| expiry_date | date | No | Initial expiry date (default: today + package days) |
| static_ip | string | No | Static IP if required (must be valid IP) |
| auto_renew | boolean | No | Auto-renew on expiry (default: false) |
Example Request:
{
"name": "Muhammad Ahmed",
"pppoe_username": "ahmed_001",
"pppoe_password": "SecurePass123",
"mobile": "03211234567",
"package_id": 5,
"area_id": 3,
"nas_id": 2
}
Response (201):
{
"data": {
"id": 502,
"name": "Muhammad Ahmed",
"pppoe_username": "ahmed_001",
"status": "active",
"expiry_date": "2026-08-13",
"package_name": "10 Mbps Unlimited",
"created_at": "2026-07-13T15:00:00Z"
},
"message": "Customer created successfully"
}
Error Responses:
401: Unauthenticated403: Nocreate_customerspermission422: Validation error (e.g., username already taken)
Notes:
After creation, RadiusService::syncCustomer() is called automatically to add the customer to FreeRADIUS tables (radcheck, radusergroup, radgroupreply). An optional WhatsApp welcome message may be sent if configured.
SEARCH CUSTOMERS¶
Method: GET
URL: /api/mobile/customers/search
Auth: Required (Bearer token)
Rate Limit: Default (60/min)
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| q | string | Yes | Search query (min 2 chars) |
| limit | integer | No | Max results to return (default: 10, max: 50) |
Response (200):
{
"data": [
{
"id": 101,
"name": "Muhammad Ali",
"pppoe_username": "ali_01",
"mobile": "03001234567",
"status": "active",
"expiry_date": "2026-08-13"
}
]
}
Notes:
Searches across name, pppoe_username, mobile, and email fields. Uses ILIKE (case-insensitive). Results are scoped to the authenticated user's accessible customers. Useful for quick customer lookup in mobile forms.
GET CUSTOMER DETAIL¶
Method: GET
URL: /api/mobile/customers/{id}
Auth: Required (Bearer token)
Rate Limit: Default (60/min)
URL Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| id | integer | Yes | Customer ID |
Response (200):
{
"data": {
"id": 101,
"name": "Muhammad Ali",
"pppoe_username": "ali_01",
"pppoe_password": "hidden",
"mobile": "03001234567",
"email": "ali@example.com",
"address": "House 5, Street 3, Johar Town",
"cnic": "35202-1234567-1",
"status": "active",
"expiry_date": "2026-08-13",
"days_remaining": 31,
"package": {
"id": 5,
"name": "10 Mbps Unlimited",
"price": 2500,
"speed_download": 10240,
"speed_upload": 5120,
"validity_days": 30
},
"dealer": {
"id": 3,
"name": "City Zone Dealer"
},
"area": {
"id": 7,
"name": "Johar Town",
"city": "Lahore"
},
"nas": {
"id": 2,
"name": "JT-Router-01",
"ip": "192.168.1.1"
},
"static_ip": null,
"online": true,
"last_online": "2026-07-13T14:45:00Z",
"billing_summary": {
"total_paid": 30000,
"last_payment": 2500,
"last_payment_date": "2026-07-01"
},
"created_at": "2025-01-15T10:00:00Z",
"updated_at": "2026-07-01T09:00:00Z"
}
}
Error Responses:
401: Unauthenticated403: Customer belongs to another dealer404: Customer not found
RECHARGE CUSTOMER¶
Method: POST
URL: /api/mobile/customers/{id}/recharge
Auth: Required (Bearer token) — requires recharge_customers permission
Rate Limit: 10 requests/minute (throttled — destructive financial operation)
URL Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| id | integer | Yes | Customer ID |
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
| package_id | integer | No | Package to apply (defaults to current package) |
| amount | number | Yes | Payment amount in PKR |
| payment_method | string | No | cash, bank_transfer, jazzcash, easypaisa (default: cash) |
| months | integer | No | Number of months to extend (default: 1) |
| notes | string | No | Transaction notes |
| send_whatsapp | boolean | No | Send WhatsApp receipt (default: true if configured) |
Example Request:
Response (200):
{
"data": {
"invoice_id": 8821,
"customer_id": 101,
"customer_name": "Muhammad Ali",
"amount_paid": 2500,
"payment_method": "cash",
"new_expiry_date": "2026-08-13",
"status": "active",
"whatsapp_sent": true,
"created_at": "2026-07-13T15:30:00Z"
},
"message": "Customer recharged successfully"
}
Error Responses:
401: Unauthenticated403: Norecharge_customerspermission404: Customer not found422: Validation error (e.g., amount must be positive)429: Rate limit exceeded (max 10 recharges/min)
Notes:
Internally calls BillingService::generateInvoice() + BillingService::recordPayment() + BillingService::renew() + RadiusService::syncCustomer(). If the customer was expired/suspended, their RADIUS account is re-enabled immediately. A WhatsApp receipt is sent if send_whatsapp is true and WhatsApp is configured. All transactions are recorded in invoices, payments, and ledger_entries tables.
EXTEND SERVICE¶
Method: POST
URL: /api/mobile/customers/{id}/extend
Auth: Required (Bearer token) — requires extend_customers permission
Rate Limit: Default (60/min)
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
| days | integer | Yes | Number of days to extend |
| reason | string | No | Reason for free extension (for audit log) |
Example Request:
Response (200):
{
"data": {
"customer_id": 101,
"previous_expiry": "2026-08-13",
"new_expiry_date": "2026-08-16",
"days_added": 3,
"reason": "Service outage compensation"
},
"message": "Service extended successfully"
}
Error Responses:
401: Unauthenticated403: Noextend_customerspermission404: Customer not found422: Days must be a positive integer
Notes:
This extends service without any payment. It creates a temp_extension record in the audit log. Used for compensating customers for outages or promotional extensions. Does not create an invoice or payment record. Triggers RadiusService::syncCustomer() to update the session.
TOGGLE CUSTOMER STATUS¶
Method: POST
URL: /api/mobile/customers/{id}/toggle
Auth: Required (Bearer token) — requires toggle_customers permission
Rate Limit: Default (60/min)
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
| action | string | Yes | enable or disable |
| reason | string | No | Reason for the action |
Example Request:
Response (200):
{
"data": {
"customer_id": 101,
"previous_status": "active",
"new_status": "disabled",
"action": "disable"
},
"message": "Customer disabled successfully"
}
Error Responses:
401: Unauthenticated403: Notoggle_customerspermission404: Customer not found422: Invalid action value
Notes:
disable sets status to disabled and removes the customer from RADIUS (they cannot connect). enable sets status back to active (if expiry is in the future) and re-syncs RADIUS. This is a hard enable/disable — different from suspension (which is automatic on expiry).
CHANGE CUSTOMER PACKAGE¶
Method: POST
URL: /api/mobile/customers/{id}/package
Auth: Required (Bearer token) — requires edit_customers permission
Rate Limit: Default (60/min)
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
| package_id | integer | Yes | New package ID |
| immediate | boolean | No | Apply immediately (true) or on next renewal (false, default) |
Example Request:
Response (200):
{
"data": {
"customer_id": 101,
"previous_package": {
"id": 5,
"name": "10 Mbps Unlimited"
},
"new_package": {
"id": 8,
"name": "20 Mbps Unlimited"
},
"applied": "immediate"
},
"message": "Package changed successfully"
}
Error Responses:
401: Unauthenticated403: Noedit_customerspermission404: Customer or package not found422: Package not available or validation error
Notes:
If immediate: true, RadiusService::syncCustomer() is called immediately to apply new speed limits. The CoA (Change of Authorization) request is sent to the NAS if the customer is currently online, forcing their session to pick up new speed limits without disconnect (if NAS supports CoA).
GET LIVE TRAFFIC¶
Method: GET
URL: /api/mobile/customers/{id}/live-traffic
Auth: Required (Bearer token)
Rate Limit: Default (60/min)
URL Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| id | integer | Yes | Customer ID |
Response (200) — Online:
{
"data": {
"customer_id": 101,
"pppoe_username": "ali_01",
"online": true,
"rx_bps": 4718592,
"tx_bps": 1048576,
"rx_human": "4.50 Mbps",
"tx_human": "1.00 Mbps",
"session_start": "2026-07-13T08:00:00Z",
"session_duration_hours": 6.5,
"session_rx_bytes": 1073741824,
"session_tx_bytes": 268435456,
"nas_ip": "192.168.1.1",
"framed_ip": "10.20.30.40"
}
}
Response (200) — Offline:
{
"data": {
"customer_id": 101,
"pppoe_username": "ali_01",
"online": false,
"rx_bps": 0,
"tx_bps": 0,
"last_seen": "2026-07-13T10:30:00Z"
}
}
Error Responses:
401: Unauthenticated403: Customer not accessible404: Customer not found
Notes:
Calls MikroTikService::liveTraffic($pppoe_username) which connects to the MikroTik router via API and fetches real-time interface statistics. This is a live call — it may take 1-3 seconds. If the customer is offline or the MikroTik is unreachable, online: false is returned without an error. Do not call this endpoint more than once every 5 seconds per customer.
GET TRAFFIC HISTORY¶
Method: GET
URL: /api/mobile/customers/{id}/traffic-history
Auth: Required (Bearer token)
Rate Limit: Default (60/min)
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| period | string | No | day, week, month (default: week) |
| from | date | No | Start date (YYYY-MM-DD) |
| to | date | No | End date (YYYY-MM-DD) |
Response (200):
{
"data": {
"customer_id": 101,
"period": "week",
"from": "2026-07-07",
"to": "2026-07-13",
"total_rx_bytes": 10737418240,
"total_tx_bytes": 2684354560,
"total_rx_human": "10.00 GB",
"total_tx_human": "2.50 GB",
"timeline": [
{
"date": "2026-07-07",
"rx_bytes": 1610612736,
"tx_bytes": 402653184,
"rx_human": "1.50 GB",
"tx_human": "384 MB"
}
],
"sessions": [
{
"session_id": "abc123",
"start": "2026-07-07T08:00:00Z",
"stop": "2026-07-07T22:00:00Z",
"duration_hours": 14,
"rx_bytes": 1610612736,
"tx_bytes": 402653184,
"nas_ip": "192.168.1.1",
"framed_ip": "10.20.30.40"
}
]
}
}
Error Responses:
401: Unauthenticated404: Customer not found
Notes:
Data is sourced from the radacct table (FreeRADIUS accounting). Historical data is available for sessions that have been closed (stop time recorded). Ongoing sessions may not appear until they terminate. Maximum date range is 90 days.
TICKETS¶
LIST TICKETS¶
Method: GET
URL: /api/mobile/tickets
Auth: Required (Bearer token)
Rate Limit: Default (60/min)
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| page | integer | No | Page number |
| status | string | No | open, in_progress, resolved, closed |
| priority | string | No | low, medium, high, critical |
| customer_id | integer | No | Filter by customer |
| assigned_to | integer | No | Filter by assignee (admin only) |
Response (200):
{
"data": [
{
"id": 301,
"title": "Internet not working",
"status": "open",
"priority": "high",
"customer": {
"id": 101,
"name": "Muhammad Ali",
"mobile": "03001234567"
},
"assigned_to": null,
"created_at": "2026-07-13T10:00:00Z",
"updated_at": "2026-07-13T10:00:00Z"
}
],
"meta": {
"current_page": 1,
"per_page": 20,
"total": 8,
"last_page": 1
}
}
Error Responses:
401: Unauthenticated
CREATE TICKET¶
Method: POST
URL: /api/mobile/tickets
Auth: Required (Bearer token)
Rate Limit: Default (60/min)
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
| customer_id | integer | Yes | Customer the ticket is for |
| title | string | Yes | Ticket title (max 255 chars) |
| description | string | Yes | Detailed description |
| priority | string | No | low, medium, high, critical (default: medium) |
| category | string | No | connectivity, billing, hardware, speed, other |
Response (201):
{
"data": {
"id": 302,
"title": "Internet not working",
"status": "open",
"priority": "high",
"customer_id": 101,
"created_at": "2026-07-13T15:00:00Z"
},
"message": "Ticket created successfully"
}
Error Responses:
401: Unauthenticated403: No access to this customer422: Validation error
PACKAGES¶
LIST PACKAGES¶
Method: GET
URL: /api/mobile/packages
Auth: Required (Bearer token)
Rate Limit: Default (60/min)
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| active_only | boolean | No | Only return active packages (default: true) |
Response (200):
{
"data": [
{
"id": 5,
"name": "10 Mbps Unlimited",
"price": 2500,
"speed_download_kbps": 10240,
"speed_upload_kbps": 5120,
"speed_display": "10 Mbps / 5 Mbps",
"validity_days": 30,
"data_limit_gb": null,
"is_unlimited": true,
"radius_group": "10mbps-unlimited",
"is_active": true
}
]
}
Notes:
Returns all packages visible to the authenticated user. Used to populate package selector dropdowns in create/recharge/change-package forms. Dealers see only packages assigned to them if package restrictions are configured.
REPORTS¶
GET REPORTS DATA¶
Method: GET
URL: /api/mobile/reports
Auth: Required (Bearer token) — requires view_reports permission
Rate Limit: Default (60/min)
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| type | string | Yes | revenue, customers, expiry, collections |
| from | date | No | Start date (YYYY-MM-DD) |
| to | date | No | End date (YYYY-MM-DD) |
| dealer_id | integer | No | Filter by dealer (super_admin only) |
| group_by | string | No | day, week, month |
Response (200) — type=revenue:
{
"data": {
"type": "revenue",
"from": "2026-07-01",
"to": "2026-07-13",
"total": 1625000,
"currency": "PKR",
"breakdown": [
{
"date": "2026-07-01",
"amount": 125000,
"transactions": 50
}
]
}
}
Error Responses:
401: Unauthenticated403: Noview_reportspermission422: Invalid report type or date range
Notes:
For dealer users, reports are automatically scoped to their own customers only. Maximum date range is 365 days. For large date ranges, consider using group_by: month to reduce response size.
NETWORK¶
GET NETWORK STATUS¶
Method: GET
URL: /api/mobile/network
Auth: Required (Bearer token) — requires view_network permission
Rate Limit: Default (60/min)
Response (200):
{
"data": {
"nas_list": [
{
"id": 1,
"name": "JT-Router-01",
"ip": "192.168.1.1",
"type": "mikrotik",
"status": "online",
"online_users": 145,
"total_users": 200,
"uptime": "45d 12h 30m",
"cpu_load": 23,
"memory_used_mb": 512,
"memory_total_mb": 2048
}
],
"olt_overview": [
{
"id": 1,
"name": "Huawei OLT-01",
"ip": "192.168.2.1",
"type": "huawei",
"status": "online",
"total_onus": 128,
"online_onus": 115,
"alarm_count": 2
}
],
"summary": {
"total_nas": 5,
"online_nas": 5,
"offline_nas": 0,
"total_online_users": 312,
"network_health": "healthy"
}
}
}
Error Responses:
401: Unauthenticated403: Noview_networkpermission
Notes:
NAS status is fetched via MikroTikService::getStatus() for each MikroTik device. OLT data is from the olt_devices table with last-polled status. The network_health field is computed: healthy if all NAS online, degraded if some offline, critical if majority offline. This endpoint may take 3-10 seconds if live NAS polling is enabled.
Mobile API vs Web Interface Differences¶
| Aspect | Mobile API | Web Interface |
|---|---|---|
| Authentication | Sanctum Bearer token | Laravel session + CSRF |
| Response format | JSON | Inertia.js (JSON + HTML) |
| Rate limiting | Enforced on destructive ops | Standard session limits |
| Permissions | Same enforcement | Same enforcement |
| Business logic | Same services | Same services |
| Real-time | Polling-based | Inertia partial reload |
| File uploads | multipart/form-data | multipart/form-data |
The same BillingService, RadiusService, MikroTikService, and WhatsAppService classes are used by both the web and mobile APIs. Business rules are identical — the API is not a shortcut around permissions.
Testing the API¶
Step 1: Login and Get Token¶
curl -X POST https://pyroradius.pyronet.com.pk/api/mobile/login \
-H "Content-Type: application/json" \
-d '{
"email": "admin@pyronet.com.pk",
"password": "change-me-on-first-login",
"device_name": "curl-test"
}'
Copy the token value from the response.
Step 2: Use the Token¶
# Set the token as a variable
TOKEN="1|your_token_here"
# Get dashboard stats
curl -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
https://pyroradius.pyronet.com.pk/api/mobile/dashboard
# List customers
curl -H "Authorization: Bearer $TOKEN" \
"https://pyroradius.pyronet.com.pk/api/mobile/customers?status=active&per_page=10"
# Get customer detail
curl -H "Authorization: Bearer $TOKEN" \
https://pyroradius.pyronet.com.pk/api/mobile/customers/101
# Recharge a customer
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"amount": 2500, "payment_method": "cash"}' \
https://pyroradius.pyronet.com.pk/api/mobile/customers/101/recharge
Step 3: Logout¶
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
https://pyroradius.pyronet.com.pk/api/mobile/logout
Using Postman¶
- Create a new collection: "PyroRadius Mobile API"
- Set base URL variable:
{{base_url}}=https://pyroradius.pyronet.com.pk/api/mobile - Add a collection variable
{{token}}(start empty) - Add a pre-request script to the Login request to automatically save the token:
- In Authorization tab of other requests, use:
Bearer {{token}}
Error Handling Best Practices¶
For Mobile App Developers¶
- Always check HTTP status code first — do not rely solely on response body parsing.
- Handle 401 globally — redirect to login screen and clear stored token.
- Handle 429 with backoff — wait
Retry-Afterheader seconds before retrying. - Handle 422 per-field — map
errors.field_nameto form field validation messages. - Handle 500 gracefully — show "Server error, please try again" — do not retry automatically.
- Token storage — use Android Keystore / iOS Secure Enclave, never SharedPreferences/UserDefaults in plaintext.
Last updated: 2026-07-13 | Maintained by: PyroNet Solutions Dev Team