Skip to content

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.

https://your-panel-host/api/services

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.

{
"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.

List services.

Permission: services.view

Query

ParamTypeDescription
is_activeboolFilter active/inactive
searchstringMatch on name / description

Response — 200 OK

{
"success": true,
"data": [ { "id": 4, "name": "8M-20G", "...": "..." } ]
}
Terminal window
curl https://panel.example.com/api/services \
-H "Authorization: Bearer ..."

Return one service.

Permission: services.view

Errors: 404 service not found.

Create.

Permission: services.create

Body

FieldTypeRequiredNotes
namestringyesUnique within tenant
pricenumberyesReseller is charged this on renew
duration_daysintyesSubscriber expiry extension
download_speedintyeskb
upload_speedintyeskb
download_speed_strstringnoAuto-derived; can override (e.g. "1.5M""1500k")
burst_download / burst_uploadstringnoMikroTik burst
monthly_quota_gbintno0 = unlimited
daily_quota_gbintno0 = unlimited
fup_tier_1_thresholdintno% of monthly quota; 50/80/100 typical
fup_tier_1_download/uploadstringnoSpeed once threshold crossed
fup_tier_2_* / fup_tier_3_*noSame shape
time_based_speed_enabledboolnoEnables the discount window
time_start / time_endstringnoHH:MM (server timezone)
time_download_ratio / time_upload_ratiointno0–100, see semantics above
mac_bindingboolnoIf true, first PPPoE Start locks the MAC
nas_idintnoDefault NAS this plan targets — used by CDN auto-fill
pool_namestringnoRADIUS Framed-Pool for IP assignment
is_activeboolnoDefault 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.

Terminal window
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.

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.

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

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.

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:

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
}
]
}

Add one binding.

Permission: services.edit

Body: { "cdn_id": 1, "download_speed": "16000k", "upload_speed": "8000k", "pcq_enabled": false }

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

Remove one binding.

Permission: services.edit

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 % usedActive tierradreply rate-limit
< tier 1 thresholdnoneservice base speed
≥ tier 1, < tier 2tier 1fup_tier_1_download / fup_tier_1_upload
≥ tier 2, < tier 3tier 2fup_tier_2_download / fup_tier_2_upload
≥ tier 3tier 3fup_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.

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) / 100
  • time_download_ratio = 100 → no bytes counted (fully free)
  • time_download_ratio = 70 → 30 % of bytes counted
  • time_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.

The handler runs every speed-like field through convertSpeedForMikrotik() so the stored value is always <integer>k:

InputStored as
20002000k
2M2000k
1.5M1500k
2000k2000k (unchanged)
2Grejected — invalid speed format
StatusmessageCause
400invalid speed formatSpeed string not parseable
400pool_name does not exist on this NASPool lookup failed against GET /api/nas/:id/pools
403services.edit deniedCaller lacks the permission
404service not foundBad id
409service still in use, move subscribers firstDelete blocked
409service name already existsUnique constraint hit

Standard envelope: { "success": false, "message": "..." }.

300 req/min/IP shared with the rest of the JWT surface. Service CRUD is low-volume — these endpoints are not a hot path.