# REST API Reference Base URL: `https://your-domain.com/api` All requests and responses are JSON (`Content-Type: application/json`), except file downloads and the logo upload (multipart). --- ## Response envelope **Success** ```json { "success": true, "message": "OK", "data": { }, "meta": { "page": 1, "per_page": 25, "total": 143, "total_pages": 6 } } ``` **Error** ```json { "success": false, "message": "Human readable message.", "errors": { "field": ["Reason"] }, "reason": "machine_code" } ``` | Code | Meaning | | --- | --- | | 200 / 201 | OK / Created | | 400 | Malformed request | | 401 | Missing or invalid token | | 403 | Authenticated but not allowed (wrong portal, suspended) | | 404 | Not found | | 419 | CSRF token mismatch | | 422 | Validation failed (`errors` populated) | | 428 | Action required (e.g. password reset pending) | | 429 | Rate limit exceeded (`Retry-After` header) | | 503 | Maintenance mode | --- ## Authentication JWT bearer tokens, HS256. ```http Authorization: Bearer ``` Access tokens live for 1 hour, refresh tokens for 14 days (30 days with *Remember me*). ### `POST /auth/login` ```json { "portal": "admin", "username": "admin", "password": "secret", "remember": true } ``` `portal` is `admin` or `reseller`. ```json { "success": true, "data": { "access_token": "...", "refresh_token": "...", "expires_in": 3600, "portal": "admin", "user": { "id": 1, "name": "Super Administrator", "username": "admin", "email": "..." } } } ``` An expired reseller receives **403**: ```json { "success": false, "message": "Your reseller account has expired. Please contact administrator.", "reason": "reseller_expired" } ``` | Endpoint | Method | Purpose | | --- | --- | --- | | `/auth/refresh` | POST | Exchange a refresh token for a new access token | | `/auth/logout` | POST | Revoke the current session | | `/auth/me` | GET | Current user + portal | | `/auth/change-password` | POST | `current_password`, `new_password` | | `/auth/profile` | PUT | Update name, email, phone, company | | `/auth/forgot-password` | POST | `portal`, `email` — always answers 200 | | `/auth/reset-password` | POST | `token`, `password` | | `/auth/sessions` | GET | Active sessions for the current user | | `/auth/sessions/{id}` | DELETE | Revoke one session | | `/csrf` | GET | Issue a CSRF token for cookie-based flows | --- ## Dashboard | Endpoint | Returns | | --- | --- | | `GET /dashboard/stats` | Admin: totals, active, expired, revoked, unused, activations_today, generated_today, total_resellers, expired_resellers, active_devices, products, customers. Reseller: quota and personal counters | | `GET /dashboard/charts` | `{ trend: [{label, generated, activated}], status_mix: [{label, total}] }` | | `GET /dashboard/activity` | Recent activity rows | --- ## Licenses | Endpoint | Method | Notes | | --- | --- | --- | | `/licenses` | GET | `page, per_page, search, status, product_id, owner, sort, direction, from, to` | | `/licenses/{id}` | GET | `{ license, devices[], logs[] }` | | `/licenses/generate` | POST | Bulk-capable generator | | `/licenses/{id}` | PUT | Edit customer, notes, product, max devices | | `/licenses/{id}` | DELETE | Soft delete (status `deleted`) | | `/licenses/bulk` | POST | `{ action: "delete"\|"revoke"\|"suspend"\|"reactivate", ids: [] }` | | `/licenses/{id}/renew` | POST | Restart the validity window | | `/licenses/{id}/extend` | POST | Add time to the current expiry | | `/licenses/{id}/transfer` | POST | Move to another reseller/customer | | `/licenses/{id}/reset-device` | POST | Release the bound device | | `/licenses/{id}/revoke` | POST | Immediate kill switch | | `/licenses/{id}/suspend` | POST | Temporary block | | `/licenses/{id}/reactivate` | POST | Back to `unused`/`activated` | | `/licenses/{id}/logs` | GET | Per-license history | | `/licenses/export` | GET | `format=csv\|xls` + the list filters | | `/licenses/import` | POST | CSV upload (admin only) | ### `POST /licenses/generate` ```json { "product_id": 1, "quantity": 25, "duration_unit": "days", "duration_amount": 30, "custom_date": null, "custom_time": null, "customer_name": "Acme Ltd", "customer_email": "buyer@acme.com", "notes": "Black Friday batch", "max_devices": 1 } ``` `duration_unit` accepts `minutes, hours, days, weeks, months, years, custom, lifetime`. With `custom`, send `custom_date` (`YYYY-MM-DD`) and optionally `custom_time` (`HH:MM`). ```json { "success": true, "message": "25 license(s) generated.", "data": { "licenses": [ { "id": 501, "license_key": "7GKD-2M4Q-XR9T-BW6H", "expires_at": "2026-08-30 15:00:00" } ] } } ``` Resellers are limited by their quota; exceeding it returns **422** with `reason: quota_exceeded`. --- ## Resellers (admin only) | Endpoint | Method | Purpose | | --- | --- | --- | | `/resellers` | GET / POST | List / create | | `/resellers/{id}` | GET / PUT / DELETE | Detail / edit / delete | | `/resellers/{id}/licenses` | GET | Licenses generated by that reseller | | `/resellers/{id}/logins` | GET | Login history | | `/resellers/{id}/activity` | GET | Activity history | | `/resellers/{id}/quota` | POST | `{ "license_limit": 750 }` or `{ "delta": 250 }` | | `/resellers/{id}/extend` | POST | `{ "duration_unit": "months", "duration_amount": 6 }` | | `/resellers/{id}/password` | POST | Reset; returns the generated password once | | `/resellers/{id}/status` | POST | `{ "status": "active"\|"suspended" }` | --- ## Products, customers, devices | Endpoint | Method | | --- | --- | | `/products` | GET / POST | | `/products/{id}` | GET / PUT / DELETE | | `/customers` | GET / POST | | `/customers/{id}` | GET / PUT / DELETE | | `/devices` | GET | | `/devices/{id}/release` | POST | | `/devices/{id}/block` | POST | --- ## Logs and reports | Endpoint | Purpose | | --- | --- | | `GET /logs/activity` | Audit trail (filterable by actor, action, date) | | `GET /logs/logins` | Login history, success and failure | | `GET /logs/licenses` | License lifecycle events | | `POST /logs/cleanup` | Manual pruning (admin only) | | `GET /reports/summary` | Daily / weekly / monthly / yearly counters | | `GET /reports/licenses` | `group=day\|week\|month\|year` series | | `GET /reports/activations` | Activation series + browser mix | | `GET /reports/top-resellers` | Ranking | | `GET /reports/expired` | `mode=expiring\|expired` | | `GET /reports/export` | `format=csv\|xls` | --- ## Settings and versions (admin only) | Endpoint | Method | Purpose | | --- | --- | --- | | `/settings` | GET | Grouped settings (secrets masked as `********`) | | `/settings` | PUT | `{ "settings": { "site_name": "..." } }` | | `/settings/logo` | POST | Multipart upload | | `/settings/test-mail` | POST | Send a test email | | `/settings/backup` | GET | Download a SQL dump | | `/settings/restore` | POST | Upload and import a dump | | `/versions` | GET / POST | Extension version registry | | `/versions/{id}` | PUT / DELETE | Edit / remove | --- ## Extension endpoints (public, no JWT) These are the only endpoints the extension calls. They are rate limited per IP and per license key. ### `POST /license/activate` ```json { "license_key": "7GKD-2M4Q-XR9T-BW6H", "fingerprint": "", "extension_version": "1.0", "browser": "Chrome 127", "os": "Windows 11", "product": "lovable-unlimited" } ``` **Valid** ```json { "success": true, "valid": true, "status": "activated", "message": "License activated successfully.", "license": { "key_masked": "7GKD-****-****-BW6H", "product": "Lovable Unlimited", "customer_name": "Acme Ltd", "activated_at": "2026-07-31 15:04:11", "expires_at": "2026-08-30 15:04:11", "lifetime": false }, "remaining": { "days": 29, "hours": 23, "minutes": 59, "seconds": 59, "text": "29d 23h 59m 59s" }, "server_time": "2026-07-31 15:04:11", "server_timestamp": 1785495851, "heartbeat_interval": 21600, "offline_grace": 86400 } ``` **Second device** ```json { "success": false, "valid": false, "status": "device_mismatch", "message": "License already activated on another device.", "reason": "device_mismatch" } ``` Other statuses: `not_found`, `expired`, `revoked`, `suspended`, `deleted`. ### `POST /license/verify` Same payload. Called on startup and whenever the UI re-checks. Returns the same envelope; a mismatching fingerprint always yields `device_mismatch`, and every call refreshes `last_seen_at` on the device binding. ### `POST /license/heartbeat` Silent periodic check (every 6 hours by default). Identical payload and response, but logged as `heartbeat` instead of `verify` and rate limited more generously. ### `POST /license/deactivate` Releases the device binding so the customer can move to a new machine (only allowed when `allow_device_reset` is enabled in Settings). ### `GET /license/version-check?version=1.0&product=lovable-unlimited` ```json { "success": true, "data": { "update_available": true, "latest_version": "1.1", "is_mandatory": false, "download_url": "https://...", "changelog": "..." } } ``` --- ## Rate limits | Bucket | Limit | | --- | --- | | default | 120 requests / 60 s | | login | 10 / 300 s | | activate | 20 / 300 s | | verify + heartbeat | 240 / 60 s | | forgot-password | 5 / 900 s | Exceeding a bucket returns **429** with a `Retry-After` header. --- ## cURL examples ```bash # Login curl -X POST https://your-domain.com/api/auth/login \ -H 'Content-Type: application/json' \ -d '{"portal":"admin","username":"admin","password":"secret"}' # Generate 10 keys valid for 6 months curl -X POST https://your-domain.com/api/licenses/generate \ -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ -d '{"product_id":1,"quantity":10,"duration_unit":"months","duration_amount":6}' # Verify from a device curl -X POST https://your-domain.com/api/license/verify \ -H 'Content-Type: application/json' \ -d '{"license_key":"7GKD-2M4Q-XR9T-BW6H","fingerprint":"abc...","extension_version":"1.0"}' ```