Endpoints — Services
A service is a plan — speed, price, duration, FUP tiers, an optional time-based discount, and a binding to a NAS + IP pool. Subscribers are assigned to exactly one service at a time. This page documents the JWT API for managing services.
Base URL
Section titled “Base URL”https://your-panel-host/api/servicesAuthentication
Section titled “Authentication”Authorization: Bearer <jwt> on every endpoint. Reseller scoping: a reseller can only see services that have been assigned to them via PUT /api/resellers/:id/assigned-services (admin-only). Admins see all.
The service object
Section titled “The service object”{ "id": 4, "name": "8M-20G", "description": "8 Mbps, 20 GB/month FUP", "price": 25.00, "duration_days": 30, "download_speed": 8000, "upload_speed": 4000, "download_speed_str": "8000k", "upload_speed_str": "4000k", "burst_download": "16000k", "burst_upload": "8000k", "monthly_quota_gb": 20, "daily_quota_gb": 0, "fup_tier_1_threshold": 50, "fup_tier_1_download": "4000k", "fup_tier_1_upload": "2000k", "fup_tier_2_threshold": 80, "fup_tier_2_download": "2000k", "fup_tier_2_upload": "1000k", "fup_tier_3_threshold": 100, "fup_tier_3_download": "1000k", "fup_tier_3_upload": "512k", "time_based_speed_enabled": true, "time_start": "00:00", "time_end": "07:00", "time_download_ratio": 70, "time_upload_ratio": 70, "mac_binding": true, "nas_id": 2, "pool_name": "8M", "framed_pool": "8M", "is_active": true, "created_at": "2025-10-01T00:00:00Z"}Speed format reminder: all speed integers are in kb (kilobits). download_speed: 8000 means 8 Mbps. The *_str fields are the strings the API will send to RADIUS as Mikrotik-Rate-Limit (e.g. 8000k).
Time-based discount semantics (v1.0.246+): time_download_ratio is the % of usage that is free during the window, not a speed boost. 100 = fully free; 70 = 30 % counted; 0 = no discount. Use Bandwidth Rules for time-window speed changes — the two are stacked at runtime.
GET /api/services
Section titled “GET /api/services”List services.
Permission: services.view
Query
| Param | Type | Description |
|---|---|---|
is_active | bool | Filter active/inactive |
search | string | Match on name / description |
Response — 200 OK
{ "success": true, "data": [ { "id": 4, "name": "8M-20G", "...": "..." } ]}curl https://panel.example.com/api/services \ -H "Authorization: Bearer ..."GET /api/services/:id
Section titled “GET /api/services/:id”Return one service.
Permission: services.view
Errors: 404 service not found.
POST /api/services
Section titled “POST /api/services”Create.
Permission: services.create
Body
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | yes | Unique within tenant |
price | number | yes | Reseller is charged this on renew |
duration_days | int | yes | Subscriber expiry extension |
download_speed | int | yes | kb |
upload_speed | int | yes | kb |
download_speed_str | string | no | Auto-derived; can override (e.g. "1.5M" → "1500k") |
burst_download / burst_upload | string | no | MikroTik burst |
monthly_quota_gb | int | no | 0 = unlimited |
daily_quota_gb | int | no | 0 = unlimited |
fup_tier_1_threshold | int | no | % of monthly quota; 50/80/100 typical |
fup_tier_1_download/upload | string | no | Speed once threshold crossed |
fup_tier_2_* / fup_tier_3_* | — | no | Same shape |
time_based_speed_enabled | bool | no | Enables the discount window |
time_start / time_end | string | no | HH:MM (server timezone) |
time_download_ratio / time_upload_ratio | int | no | 0–100, see semantics above |
mac_binding | bool | no | If true, first PPPoE Start locks the MAC |
nas_id | int | no | Default NAS this plan targets — used by CDN auto-fill |
pool_name | string | no | RADIUS Framed-Pool for IP assignment |
is_active | bool | no | Default true |
The handler runs convertSpeedForMikrotik() so plain numbers (2000), 2M strings, and decimal forms (1.5M) all normalise to 2000k / 1500k.
Response — 201 Created — full service object.
curl -X POST https://panel.example.com/api/services \ -H "Authorization: Bearer ..." \ -H "Content-Type: application/json" \ -d '{ "name":"4M-12G","price":15,"duration_days":30, "download_speed":4000,"upload_speed":2000, "monthly_quota_gb":12, "fup_tier_1_threshold":80,"fup_tier_1_download":"2000k","fup_tier_1_upload":"1000k", "fup_tier_2_threshold":100,"fup_tier_2_download":"1000k","fup_tier_2_upload":"512k", "nas_id":2,"pool_name":"4M" }'Errors: 409 service name already exists, 400 invalid speed format.
PUT /api/services/:id
Section titled “PUT /api/services/:id”Update. Whitelisted fields only — unknown keys ignored. Speed-string conversion runs on every update too.
Permission: services.edit
Changing the speed fields does not retroactively change live subscribers — their next reconnect picks up the new attributes. To force a change immediately, follow with a bulk-action: disconnect.
DELETE /api/services/:id
Section titled “DELETE /api/services/:id”Soft delete. Blocked if any non-deleted subscriber still references the id — the handler returns 409 service still in use, move subscribers first.
Permission: services.delete
Duplicate (UI-only convenience)
Section titled “Duplicate (UI-only convenience)”The “Duplicate” button in the Services page is a pure client-side copy: it pre-fills the create form with the existing service’s values and appends (Copy) to the name. There is no dedicated server endpoint — the resulting POST /api/services is the same one documented above.
Service-CDN configuration
Section titled “Service-CDN configuration”Each service can have zero or more service-CDN rows that bind it to a specific CDN config + speed pair. These live under the service resource:
GET /api/services/:id/cdns
Section titled “GET /api/services/:id/cdns”Permission: services.view
{ "success": true, "data": [ { "id": 22, "service_id": 4, "cdn_id": 1, "download_speed": "16000k", "upload_speed": "8000k", "time_based_speed_enabled": true, "pcq_enabled": false } ]}POST /api/services/:id/cdns
Section titled “POST /api/services/:id/cdns”Add one binding.
Permission: services.edit
Body: { "cdn_id": 1, "download_speed": "16000k", "upload_speed": "8000k", "pcq_enabled": false }
PUT /api/services/:id/cdns
Section titled “PUT /api/services/:id/cdns”Replace the full list of bindings for this service. The handler diffs against the existing rows and creates/updates/deletes as needed.
Permission: services.edit
DELETE /api/services/:id/cdns/:cdnId
Section titled “DELETE /api/services/:id/cdns/:cdnId”Remove one binding.
Permission: services.edit
FUP tiers — how they’re enforced
Section titled “FUP tiers — how they’re enforced”QuotaSync runs every 30 s. For each online subscriber it compares daily_quota_used and monthly_quota_used against the service’s three FUP thresholds (% of monthly_quota_gb).
Subscriber % used | Active tier | radreply rate-limit |
|---|---|---|
| < tier 1 threshold | none | service base speed |
| ≥ tier 1, < tier 2 | tier 1 | fup_tier_1_download / fup_tier_1_upload |
| ≥ tier 2, < tier 3 | tier 2 | fup_tier_2_download / fup_tier_2_upload |
| ≥ tier 3 | tier 3 | fup_tier_3_download / fup_tier_3_upload |
The new rate-limit is written to radreply, applied immediately via MikroTik API on the live queue, and falls back to CoA if the API fails. Speeds are sent in kb format (8000k) — never multiply by 1000.
reset_fup (single or bulk) sets fup_level back to 0 and restores the service base speed. renew does the same plus resets the monthly counter.
Time-based discount window
Section titled “Time-based discount window”When time_based_speed_enabled = true, between time_start and time_end each byte_delta measured by QuotaSync is reduced before being added to the counter:
counted_bytes = delta_bytes * (100 - time_download_ratio) / 100time_download_ratio = 100→ no bytes counted (fully free)time_download_ratio = 70→ 30 % of bytes countedtime_download_ratio = 0→ no discount (normal)
The window’s clock is the server timezone configured in system_preferences.system_timezone.
time_upload_ratio is the equivalent for upload bytes. Most operators set both equal.
Speed-string normalisation
Section titled “Speed-string normalisation”The handler runs every speed-like field through convertSpeedForMikrotik() so the stored value is always <integer>k:
| Input | Stored as |
|---|---|
2000 | 2000k |
2M | 2000k |
1.5M | 1500k |
2000k | 2000k (unchanged) |
2G | rejected — invalid speed format |
Errors
Section titled “Errors”| Status | message | Cause |
|---|---|---|
| 400 | invalid speed format | Speed string not parseable |
| 400 | pool_name does not exist on this NAS | Pool lookup failed against GET /api/nas/:id/pools |
| 403 | services.edit denied | Caller lacks the permission |
| 404 | service not found | Bad id |
| 409 | service still in use, move subscribers first | Delete blocked |
| 409 | service name already exists | Unique constraint hit |
Standard envelope: { "success": false, "message": "..." }.
Rate limits
Section titled “Rate limits”300 req/min/IP shared with the rest of the JWT surface. Service CRUD is low-volume — these endpoints are not a hot path.
Related pages
Section titled “Related pages”- Endpoints — Subscribers — assign subscribers to a service
- FUP Counters — runtime view of FUP tiers
- Bandwidth Rules — time-window speed changes (stacked on top of services)
- NAS / Routers — pools the
pool_namefield references