TR Store API
The TR Store REST API lets clients validate licenses for private products, bots, and server assets.
All API responses are in JSON format. The API uses JWT Bearer tokens for authentication.
Base URL
# Development
http://localhost:3000/api
# Production
https://your-server.com/api
http://localhost:3000/api
# Production
https://your-server.com/api
Authentication
Most endpoints require a JWT token in the Authorization header:
Authorization: Bearer <your_jwt_token>
Get your session token by calling POST /auth/login — expires in 7 days.
Get your API access token (for license validation) from Settings → API Access Token — expires in 365 days.
Two token types:
• Session token — issued on login, used for dashboard/admin endpoints.
• API access token — generated in Settings, used only for
• Session token — issued on login, used for dashboard/admin endpoints.
• API access token — generated in Settings, used only for
POST /licenses/validate.
GET
/health
Check if the API server is running. No authentication required.
Response
{ "success": true, "message": "Licensing API is running", "timestamp": "2025-01-01T00:00:00.000Z" }
Auth Endpoints
POST
/auth/login
Authenticate with email and password. Returns a JWT session token valid for 7 days.
Request Body
{
"email": "[email protected]",
"password": "YourPassword123",
"two_factor_code": "123456", // required if 2FA is enabled
"backup_code": "BACKUP-CODE", // alternative to two_factor_code
"verify_code": "654321" // required if email not yet verified
}
Success Response
{
"success": true,
"token": "eyJ...",
"user": {
"id": 1,
"email": "[email protected]",
"role": "client",
"discord_username": "username",
"two_factor_enabled": false
}
}
Special Responses
// Email not yet verified (403)
{ "success": false, "requires_email_verification": true, "message": "..." }
// 2FA required (401)
{ "success": false, "requires_2fa": true, "message": "Two-factor authentication code required" }
POST
/auth/register
Register a new client account. Password must be 8+ chars with uppercase, lowercase, and a number. Account requires admin approval before login is allowed.
Request Body
Response (201)
{ "success": true, "pending_approval": true, "message": "Registration submitted. Please wait for admin approval..." }
GET
/auth/me
Auth Required
Get the currently authenticated user's profile including Discord info, API token metadata, and 2FA status.
Response
{
"success": true,
"user": {
"id": 1, "email": "[email protected]", "role": "client",
"discord_id": "123456789", "discord_username": "username",
"is_active": 1, "registration_status": "approved",
"api_token": "eyJ...", "api_token_created_at": "2025-01-01T00:00:00.000Z",
"two_factor_enabled": false
}
}
GET
/auth/ip-history
Auth Required
Get the last 20 login IP addresses and user agents for the current user.
GET
/auth/api-token
Auth Required
Get the current API access token and its metadata (created_at, last_used_at).
POST
/auth/api-token/rotate
Auth Required
Generate a new API access token (365-day expiry). The old token is immediately invalidated.
Response
{ "success": true, "token": "eyJ...", "created_at": "2025-01-01T00:00:00.000Z" }
DELETE
/auth/api-token
Auth Required
Revoke the current API access token. License validation will fail until a new token is generated.
GET
/auth/verify?token=<hex>
One-click email verification link. Called automatically when the user clicks the link in their welcome email.
Query Params
token=<64-char hex string from email>
POST
/auth/verify/resend
Resend the email verification code to the given address.
Request Body
{ "email": "[email protected]" }
GET
/auth/2fa/status
Auth Required
Get the 2FA status for the current user.
Response
{ "success": true, "enabled": false, "has_pending_setup": false, "remaining_backup_codes": 8 }
POST
/auth/2fa/setup
Auth Required
Start TOTP 2FA setup. Returns a secret, otpauth_url (for QR code), and 8 backup codes. Finish setup with
/auth/2fa/enable.Response
{ "success": true, "secret": "BASE32SECRET", "otpauth_url": "otpauth://totp/...", "backup_codes": ["CODE1", "..."] }
POST
/auth/2fa/enable
Auth Required
Confirm and enable 2FA with a 6-digit TOTP code from your authenticator app.
Request Body
{ "code": "123456" }
POST
/auth/2fa/disable
Auth Required
Disable 2FA. Requires a valid TOTP code or a backup code.
Request Body
{ "code": "123456" } // or use "backup_code": "BACKUP-CODE"
POST
/auth/2fa/regenerate-backup-codes
Auth Required
Regenerate 8 new backup codes. Requires a valid TOTP code. Old backup codes are invalidated.
Request Body
{ "code": "123456" }
POST
/auth/2fa/reset-request
Auth Required
Submit a 2FA reset request to an admin (for when you've lost your authenticator and backup codes).
GET
/auth/2fa/reset-request/status
Auth Required
Check the status of the current user's latest 2FA reset request.
GET
/auth/2fa/reset-requests/pending
Auth Required
Admin Only
List all pending 2FA reset requests (admin only).
POST
/auth/2fa/reset-requests/:id/review
Auth Required
Admin Only
Approve or reject a 2FA reset request. Approving will remove 2FA from the user's account.
Request Body
{ "action": "approve" } // or "reject"
License Endpoints
POST
/licenses/validate
API Access Token Required
Validate a license using your private API access token from Settings. The token only validates licenses owned by that account.
All validation attempts are logged — success and failure records are inserted into
All validation attempts are logged — success and failure records are inserted into
license_validations with the exact failure reason. The license owner is notified of failed attempts.
Headers
Authorization: Bearer YOUR_API_ACCESS_TOKEN
Request Body
// Option 1: License Key
{ "license_key": "ABCD1234-EFGH5678-IJKL9012-MNOP3456" }
// Option 2: Server IP
{ "server_ip": "192.168.1.100" }
// Option 3: Discord Server ID
{ "discord_server_id": "123456789012345678" }
{ "license_key": "ABCD1234-EFGH5678-IJKL9012-MNOP3456" }
// Option 2: Server IP
{ "server_ip": "192.168.1.100" }
// Option 3: Discord Server ID
{ "discord_server_id": "123456789012345678" }
Success Response
{
"valid": true,
"product_name": "My Script",
"product_type": "fivem_script",
"expires_at": null,
"validation_method": "license_key",
"status": { "license_id": 1, "is_active": true, "...": "..." }
}
Failure Response
{
"valid": false,
"reason": "License has expired", // exact error message
"status": { "...": "..." }
}
Possible Failure Reasons
"License not found" // 404 — key/IP/discord ID doesn't exist
"API access token is required" // 401 — no token provided
"Invalid API access token" // 401 — token type mismatch
"API token owner is inactive" // 401 — account disabled
"API access token has been revoked" // 401 — token was deleted
"Invalid or expired API access token" // 401 — JWT verify failed
"This API token does not belong to the license owner" // 403 — wrong account
"This license must be validated using <method>" // wrong validation method used
"License is disabled" // license toggled off
"License has expired" // expiry date passed
"Rate limit exceeded" // 120 req/min exceeded
GET
/licenses
Auth Required
Get all licenses. Clients see only their own licenses. Admins see all licenses with owner email.
GET
/licenses/:id
Auth Required
Get a specific license by ID. Clients can only access their own licenses.
POST
/licenses
Auth Required
Admin Only
Create a new license and assign it to a user.
Request Body
{
"user_id": 1,
"product_name": "My Script",
"product_type": "fivem_script", // "fivem_script" | "discordjs_bot"
"validation_method": "license_key", // "license_key" | "server_ip" | "discord_server_id"
"server_ip": "1.2.3.4", // required if validation_method = server_ip
"discord_server_id": "123...", // required if validation_method = discord_server_id
"expires_at": "2026-01-01 00:00:00", // optional, null = never expires
"notes": "Optional notes",
"download_link": "https://..." // optional
}
Response (201)
{ "success": true, "message": "License created", "license_key": "XXXX-XXXX-XXXX-XXXX", "id": 1 }
PUT
/licenses/:id
Auth Required
Admin Only
Update a license. All fields are optional — only provided fields are updated.
DELETE
/licenses/:id
Auth Required
Admin Only
Permanently delete a license.
PATCH
/licenses/:id/toggle
Auth Required
Admin Only
Toggle a license's active status on/off.
Response
{ "success": true, "is_active": 1 }
POST
/licenses/cleanup-expired
Auth Required
Admin Only
Manually trigger deletion of all expired licenses. This also runs automatically every 60 seconds.
Response
{ "success": true, "removed": 3 }
GET
/licenses/validation-summary
Auth Required
Get validation statistics and the 10 most recent validation attempts. Clients see only their own data; admins see all.
Response
{
"success": true,
"summary": {
"totals": { "total_validations": 100, "valid_count": 90, "invalid_count": 10, "last_24h": 5, "last_7d": 30 },
"recent": [ { "id": 1, "is_valid": 1, "failure_reason": null, "ip_address": "1.2.3.4", "...": "..." } ]
}
}
GET
/licenses/validation-activity?user_id=<id>&limit=<n>
Auth Required
Admin Only
Get detailed validation history for a specific user. Max 500 records.
GET
/licenses/client-edit-config
Auth Required
Get the current configuration for client identifier editing (enabled/disabled, requires approval).
Response
{ "success": true, "config": { "enabled": true, "requiresApproval": false } }
POST
/licenses/:id/client-identifier-update
Auth Required
Client-only endpoint to update their own license identifier (key/IP/discord ID). If approval is required, a request is created for admin review.
Request Body
{
"license_key": "NEW-KEY-HERE", // depends on validation_method
"server_ip": "1.2.3.4",
"discord_server_id": "123...",
"regenerate": true // auto-generate a new license_key
}
GET
/licenses/identifier-change-requests/my
Auth Required
Get all identifier change requests submitted by the current user.
GET
/licenses/identifier-change-requests/pending
Auth Required
Admin Only
List all pending identifier change requests (admin only).
POST
/licenses/identifier-change-requests/:id/review
Auth Required
Admin Only
Approve or reject an identifier change request.
Request Body
{ "action": "approve" } // or "reject"
Client Endpoints
GET
/clients
Auth Required
Admin Only
List all users (clients and admins) ordered by creation date.
GET
/clients/:id
Auth Required
Admin Only
Get a specific client including their licenses and last 10 IP history entries.
POST
/clients
Auth Required
Admin Only
Create a new client account. If discord_id is provided, Discord username and avatar are auto-fetched.
Request Body
{ "email": "[email protected]", "password": "Pass123", "discord_id": "123456789", "role": "client" }
PUT
/clients/:id
Auth Required
Admin Only
Update a client's active status, role, or Discord ID.
Request Body
{ "is_active": true, "role": "client", "discord_id": "123456789" }
DELETE
/clients/:id
Auth Required
Admin Only
Permanently delete a client account. Cannot delete your own account.
GET
/clients/registrations/pending
Auth Required
Admin Only
List all pending registration requests awaiting admin approval.
GET
/clients/registrations/rejected
Auth Required
Admin Only
List all rejected registrations.
POST
/clients/registrations/:id/review
Auth Required
Admin Only
Approve or reject a pending registration.
Request Body
{ "action": "approve" } // or "reject"
POST
/clients/registrations/:id/reenable
Auth Required
Admin Only
Re-enable a previously rejected registration.
POST
/clients/change-request
Auth Required
Submit an email or password change request. Admins apply changes directly; client changes require admin approval.
Request Body
{
"request_type": "email_change", // or "password_change"
"new_value": "[email protected]",
"target_user_id": 2 // admin only — change another user's data
}
GET
/clients/change-requests/my
Auth Required
Get all change requests submitted by the current user with a summary of pending status.
GET
/clients/change-requests/pending
Auth Required
Admin Only
List all pending email/password change requests (admin only).
POST
/clients/change-requests/:id/review
Auth Required
Admin Only
Approve or reject an email/password change request.
Request Body
{ "action": "approve" } // or "reject"
Notification Endpoints
GET
/notifications
Auth Required
Get the last 30 notifications for the current user, plus unread count.
POST
/notifications/:id/read
Auth Required
Mark a single notification as read.
POST
/notifications/read-all
Auth Required
Mark all unread notifications as read.
GET
/notifications/user/:userId?limit=100
Auth Required
Admin Only
Get notifications for a specific user (admin only). Max 500 records.
Log Endpoints
GET
/logs?page=1&limit=50&severity=warning&action=login
Auth Required
Admin Only
Get paginated system logs. Filter by severity (info/warning/error) or action keyword.
GET
/logs/stats
Auth Required
Admin Only
Get log statistics: total count, today's count, breakdown by severity, and top 10 recent actions.
Discord Endpoints
GET
/discord/user/:discord_id
Auth Required
Admin Only
Fetch Discord user info (username, avatar, discriminator) using the bot token configured in
.env.Editor Endpoints
GET
/editor/my-exports
Auth Required
Get the last 25 folder exports shared with the current client user.
GET
/editor/exports
Auth Required
Admin Only
Get the last 25 editor exports across all clients (admin only).
POST
/editor/send-folder?client_user_id=<id>&folder_name=<name>&zip_name=<name.zip>
Auth Required
Admin Only
Upload a ZIP file to Gofile and share the download link with a client. Body must be raw
application/zip. Max 100MB.Headers
Content-Type: application/zip
Response
{ "success": true, "link": "https://gofile.io/d/...", "export_id": 1 }
Realtime Stream
GET
/stream/dashboard?token=<jwt>
Auth Required (query param)
Server-Sent Events (SSE) stream for real-time dashboard updates. Pass your session JWT as a query parameter. Emits
ready, heartbeat (every 25s), and dashboard events.Example
const es = new EventSource(`/api/stream/dashboard?token=${token}`);
es.addEventListener('dashboard', e => console.log(JSON.parse(e.data)));
Integration Examples
FiveM Integration (Lua)
-- server.lua
local API_URL = "http://your-server.com/api/licenses/validate"
local API_TOKEN = "YOUR_API_ACCESS_TOKEN"
local LICENSE_KEY = "YOUR-LICENSE-KEY-HERE"
local function ValidateLicense()
PerformHttpRequest(API_URL, function(status, response)
if status ~= 200 then
print("[TR Store] HTTP error: " .. tostring(status))
StopResource(GetCurrentResourceName())
return
end
local ok, data = pcall(json.decode, response)
if not ok then
print("[TR Store] Failed to parse response")
StopResource(GetCurrentResourceName())
return
end
if data and data.valid then
print("[TR Store] License valid: " .. tostring(data.product_name))
else
-- Server has already logged this attempt automatically
print("[TR Store] License invalid: " .. tostring(data and data.reason or "Unknown error"))
StopResource(GetCurrentResourceName())
end
end, "POST", json.encode({ license_key = LICENSE_KEY }), {
["Content-Type"] = "application/json",
["Authorization"] = "Bearer " .. API_TOKEN
})
end
AddEventHandler("onResourceStart", function(resourceName)
if resourceName == GetCurrentResourceName() then
ValidateLicense()
end
end)
Discord.js Integration (JavaScript)
// validate.js
const API_URL = 'http://your-server.com/api/licenses/validate';
const API_TOKEN = 'YOUR_API_ACCESS_TOKEN';
async function validateLicense(discordServerId) {
try {
const res = await fetch(API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_TOKEN}`
},
body: JSON.stringify({ discord_server_id: discordServerId })
});
const data = await res.json();
if (data.valid) {
return data; // proceed normally
} else {
// Server has already logged this attempt automatically
console.error('License invalid:', data.reason);
process.exit(1);
}
} catch (err) {
console.error('Validation request failed:', err.message);
process.exit(1);
}
}
client.once('ready', async () => {
await validateLicense(client.guilds.cache.first()?.id);
});
Python Integration
import sys, json, urllib.request, urllib.error
API_TOKEN = "YOUR_API_ACCESS_TOKEN"
BASE_URL = "http://your-server.com"
LICENSE_KEY = "YOUR-LICENSE-KEY"
def validate_license():
body = json.dumps({"license_key": LICENSE_KEY}).encode("utf-8")
req = urllib.request.Request(
f"{BASE_URL}/api/licenses/validate",
data=body, method="POST",
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {API_TOKEN}"
}
)
try:
with urllib.request.urlopen(req) as resp:
return json.loads(resp.read().decode("utf-8"))
except urllib.error.HTTPError as e:
return json.loads(e.read().decode("utf-8"))
if __name__ == "__main__":
result = validate_license()
if result.get("valid"):
print("License valid:", result.get("product_name"))
else:
# Server has already logged this attempt automatically
print("License invalid:", result.get("reason"))
sys.exit(1)