Proudly / API Documentation

Application API

Complete reference for the Proudly Application API. All requests go through POST /api/v1 with an action parameter.

POST /api/v1

Initiates a handshake session for your application. Returns a unique session identifier and encryption secret.

How it works

01

Send init

POST to /api/v1 with your admin ID, app ID, and action=init

02

Server validates

Verifies your app exists and the owner account is active

03

Session created

A secure session ID and encryption secret are generated

04

Encrypt requests

Use the returned secret to encrypt all subsequent API calls

Request parameters

Parameters can be sent as query string, form body, or as part of a route string in the format {admin_id}/{app_id}&action=init.

Parameter Type Required Description
admin_id string Required Your admin user ID (found in your dashboard settings)
app_id string Required The application ID you want to initialize a session for
action string Required Must be init
secret_key string Required Your app's secret key (found in dashboard). Required for unencrypted requests; not needed for encrypted requests.
Route format. The API accepts a compact route format: {admin_id}/{app_id}&init — either as a query string or in the request body. The explicit action=init parameter is also supported.

Encrypted request format

For production use, the init request should be encrypted. The URL includes plaintext hints for fast key lookup, while the request body is AES-encrypted.

URL format

URL
POST https://proudlyauthentication.com/api/v1/?aid=ADMIN_ID&appid=APP_ID

The ?aid= and &appid= query parameters are plaintext hints (only needed for encrypted requests) that allow the server to resolve the correct encryption key in O(1) instead of brute-forcing all keys. For unencrypted requests these are not needed since the admin ID and app ID are already visible in the body. In C++, these are typically wrapped with XOR() for compile-time string obfuscation.

Encrypted body

Format
encrypt(adminID, enc_key, iv) / encrypt(appID, enc_key, iv) / encrypt("&action=init", enc_key, iv) &iv=IV
ComponentDescription
enc_keyYour app's encryption key (set in dashboard). Used only for init — after init, use the returned secret instead.
ivA random string generated per-session. Client generates it, sends it in plaintext as &iv=, and uses it for all encrypt/decrypt in that session.
/Slash separator between each encrypted segment. Each segment is individually encrypted and hex-encoded.

Encryption details

PropertyValue
AlgorithmAES-256-CBC with PKCS7 padding
Key derivationSHA256(enc_key).substring(0, 32) — first 32 hex chars as UTF-8 bytes
IV derivationSHA256(iv).substring(0, 16) — first 16 hex chars as UTF-8 bytes
Output encodingLowercase hexadecimal

After init

The init response returns a session_id and secret. All subsequent requests must:

  • Use the secret as the encryption key (replaces enc_key)
  • Use the same iv from the init request
  • Include sessionid in every encrypted request body
  • The response is also encrypted with the same secret + iv — decrypt it to read the JSON
XOR() macro. In C++ implementations, string literals like "?aid=" and "&appid=" are wrapped with XOR() — a compile-time string obfuscation macro that prevents static analysis from extracting URL patterns from your binary. The macro has no effect on the actual string value sent to the server.

Code examples

import requests # Your credentials from the dashboard ADMIN_ID = "your_admin_id" APP_ID = "your_app_id" SECRET_KEY = "your_secret_key" BASE_URL = "https://proudlyauthentication.com" # Initialize handshake response = requests.post( f"{BASE_URL}/api/v1", data=f"{ADMIN_ID}/{APP_ID}&init&secret_key={SECRET_KEY}" ) result = response.json() if result["success"]: session_id = result["session_id"] print(f"Session: {session_id}") # Use session_id in subsequent requests response = requests.post( f"{BASE_URL}/api/v1", data=f"{ADMIN_ID}/{APP_ID}&getglobalvariable&sessionid={session_id}&varid=my_variable" ) print(f"Value: {response.json()['value']}") else: print(f"Error: {result['message']}")
const ADMIN_ID = "your_admin_id"; const APP_ID = "your_app_id"; const SECRET_KEY = "your_secret_key"; const BASE_URL = "https://proudlyauthentication.com"; // Initialize handshake const response = await fetch(`${BASE_URL}/api/v1`, { method: "POST", headers: { "Content-Type": "text/plain" }, body: `${ADMIN_ID}/${APP_ID}&init&secret_key=${SECRET_KEY}` }); const result = await response.json(); if (result.success) { const { session_id } = result; console.log(`Session: ${session_id}`); // Use session_id in subsequent requests const varRes = await fetch(`${BASE_URL}/api/v1`, { method: "POST", headers: { "Content-Type": "text/plain" }, body: `${ADMIN_ID}/${APP_ID}&getglobalvariable&sessionid=${session_id}&varid=my_variable` }); const varData = await varRes.json(); console.log(`Value: ${varData.value}`); } else { console.error(`Error: ${result.message}`); }
using System.Net.Http; using System.Text.Json; var adminId = "your_admin_id"; var appId = "your_app_id"; var secretKey = "your_secret_key"; var baseUrl = "https://proudlyauthentication.com"; using var client = new HttpClient(); // Initialize handshake var content = new StringContent( $"{adminId}/{appId}&init&secret_key={secretKey}" ); var response = await client.PostAsync( $"{baseUrl}/api/v1", content ); var json = await response.Content .ReadAsStringAsync(); var result = JsonSerializer .Deserialize<JsonElement>(json); if (result.GetProperty("success").GetBoolean()) { var sessionId = result .GetProperty("session_id").GetString(); // Use session_id in subsequent requests var varContent = new StringContent( $"{adminId}/{appId}&getglobalvariable&sessionid={sessionId}&varid=my_variable" ); var varResponse = await client.PostAsync( $"{baseUrl}/api/v1", varContent ); var varJson = await varResponse.Content .ReadAsStringAsync(); var varResult = JsonSerializer .Deserialize<JsonElement>(varJson); }
curl -X POST https://proudlyauthentication.com/api/v1 \ -d "your_admin_id/your_app_id&init&secret_key=your_secret_key" # Use the session_id from init response curl -X POST https://proudlyauthentication.com/api/v1 \ -d "your_admin_id/your_app_id&getglobalvariable&sessionid=SESSION_ID_FROM_INIT&varid=my_variable"

Response

Success

200 OK
JSON
// Encrypted init request → encrypted response with secret { "success": true, "session_id": "aB3kL9mNqR2xT7wZ...", "secret": "xK4pQ8rS1vU6yW..." } // Unencrypted init request → plain JSON, no secret { "success": true, "session_id": "aB3kL9mNqR2xT7wZ..." }
Field Type Description
success boolean Always true on success
session_id string Unique session identifier (URL-safe base64, 32 chars). Use this in subsequent requests.
secret string Per-session encryption key (URL-safe base64, 22 chars). Only returned when the init request itself is encrypted. Use as the AES key for all subsequent requests.

Error responses

400 Bad Request
JSON
{ "success": false, "message": "Missing or invalid route" }

Returned when admin_id, app_id, or action is missing or malformed.

404 Not Found
JSON
{ "success": false, "message": "Application not found" }

The specified app_id does not exist under the given admin_id.

503 Service Unavailable
JSON
{ "success": false, "message": "This application is currently unavailable" }

The application owner's account is frozen or banned.

Security

Keep your secret safe. The secret returned by init is used to encrypt all subsequent API requests with AES-256-CBC. Never expose it in client-side code, logs, or version control.
  • Handshake sessions are short-lived and automatically expire
  • Each session gets a unique encryption key — no two sessions share the same secret
  • Maximum request size is 10 KB
  • All responses include security headers (HSTS, X-Frame-Options, CSP)
  • Failed handshakes are tracked to prevent brute-force attempts

Login

Authenticate a customer using their credentials within an established handshake session. Supports HWID binding, subscription validation, and IP-based security.

POST /api/v1 action=login
ParameterTypeRequiredDescription
sessionidstringRequiredSession ID from init handshake
usernamestringRequiredCustomer username (supports hex encoding)
passwordstringRequiredCustomer password (supports hex encoding)
hwidstringOptionalHardware ID for device binding
subscriptionstringOptionalRequired subscription hash to validate

Success response

JSON
{ "success": true, "subscriptions": [/* subscription hashes */], "message": "login_success" }

Error responses

400 Bad Request
JSON
{ "success": false, "message": "username, password, sessionid, and hwid are required" }

Missing required credentials. Also returned for invalid content type or empty HWID value.

401 Unauthorized
JSON
{ "success": false, "message": "Invalid username or password" }

Username not found or password does not match.

403 Forbidden
JSON
{ "success": false, "message": "Your account has been banned." }

Account is banned (may include ban_note). Also returned for HWID mismatch, IP blacklisted, email verification required, no active subscriptions, or missing required subscription.

404 Not Found
JSON
{ "success": false, "message": "Session handshake not found" }

No handshake record found for the provided session ID.

423 Locked
JSON
{ "success": false, "message": "Too many failed login attempts. Please try again later." }

Session locked or account locked due to repeated failed login attempts.

Register

Create a new customer account with license key redemption. Supports optional email verification.

POST /api/v1 action=register
ParameterTypeRequiredDescription
sessionidstringRequiredSession ID from init handshake
usernamestringRequiredDesired username (supports hex encoding)
emailstringRequiredCustomer email address
passwordstringRequiredAccount password (supports hex encoding)
keystringRequiredLicense key to redeem
hwidstringOptionalHardware ID for device binding
locationstringOptionalUser location (max 120 chars)

Success response

JSON
{ "success": true, "subscriptions": [/* subscription hashes */] }
If email verification is enabled, the response includes "verification_required": true instead of subscriptions.

Error responses

400 Bad Request
JSON
{ "success": false, "message": "username is required" }

Missing required fields (username, email, password, key). Also returned for invalid email format, username/email already exists, invalid/used/upgrade-only registration key, or location exceeding 120 characters.

403 Forbidden
JSON
{ "success": false, "message": "Access from your IP address has been blocked by the application owner." }

Client IP is on the application's blacklist. Also returned when the registration key is banned.

404 Not Found
JSON
{ "success": false, "message": "Session handshake not found" }

No handshake record found for the provided session ID.

423 Locked
JSON
{ "success": false, "message": "Session locked" }

Handshake session is locked due to too many prior failures.

Session Checkup

Heartbeat endpoint that validates an authenticated session, checks IP binding, and extends its lifetime.

POST /api/v1 action=sessioncheckup
ParameterTypeRequiredDescription
sessionidstringRequiredSession ID from init handshake

Success response

JSON
{ "success": true, "expires_at": "2026-02-15T16:00:00Z", "expiry": 3600000, "beats": 42 }

Error responses

400 Bad Request
JSON
{ "success": false, "message": "sessionid is required" }

No session ID was provided in the request.

401 Unauthorized
JSON
{ "success": false, "message": "Session invalidated for security reasons" }

IP binding mismatch — the client IP differs from the IP bound to the session.

403 Forbidden
JSON
{ "success": false, "message": "Session not authenticated" }

The handshake is not in an authenticated state or has no active session.

404 Not Found
JSON
{ "success": false, "message": "Session handshake not found" }

No handshake or active session found for the provided session ID.

Upgrade

Redeem a license key to add or extend subscriptions for an authenticated user.

POST /api/v1 action=upgrade
ParameterTypeRequiredDescription
sessionidstringRequiredSession ID from init handshake
keystringRequiredLicense key to redeem

Success response

JSON
{ "success": true, "message": "license_key_success", "subscriptions": [/* updated subscriptions */], "customer": { /* customer object */ } }

Error responses

400 Bad Request
JSON
{ "success": false, "message": "A license key is required" }

Missing key parameter, key shorter than 8 characters, key already redeemed, or upgrade key incompatible with current subscriptions.

403 Forbidden
JSON
{ "success": false, "message": "This key has been banned and cannot be redeemed." }

The license key has been banned by the application owner. Also returned when the session is not authenticated.

404 Not Found
JSON
{ "success": false, "message": "License key not recognized" }

The provided key does not exist in the application's key store. Also returned when the session or customer is not found.

Verify Email

Verify a customer's email address using the verification code sent during registration.

POST /api/v1 action=verify
ParameterTypeRequiredDescription
sessionidstringRequiredSession ID from init handshake
codestringRequiredVerification code from email

Success response

JSON
{ "success": true, "message": "verification_success" }

Error responses

400 Bad Request
JSON
{ "success": false, "message": "Invalid or expired verification code" }

Missing verification code, code has expired, code does not match (includes attempts_remaining), or no customer associated with the session.

404 Not Found
JSON
{ "success": false, "message": "Session handshake not found" }

No handshake found for the provided session ID.

409 Conflict
JSON
{ "success": false, "message": "Email is already verified" }

The customer's email address has already been verified.

423 Locked
JSON
{ "success": false, "message": "Maximum attempts exceeded. Please request a new code." }

Too many incorrect verification attempts. A new code must be requested via the resend action.

Resend Verification

Resend the email verification code. Rate limited to 5 resends maximum.

POST /api/v1 action=resend
ParameterTypeRequiredDescription
sessionidstringRequiredSession ID from init handshake

Success response

JSON
{ "success": true, "message": "resend_verification_success" }

Error responses

400 Bad Request
JSON
{ "success": false, "message": "sessionid is required" }

No session ID was provided. Also returned when no customer is associated with the session.

404 Not Found
JSON
{ "success": false, "message": "Session handshake not found" }

No handshake found for the provided session ID.

429 Too Many Requests
JSON
{ "success": false, "message": "A verification code is still active. Please wait before requesting a new one.", "retry_after": 120 }

The current verification code hasn't expired yet (includes retry_after in seconds). Also returned when the resend limit of 5 has been reached.

Reset Password — Request Code

Request a password-reset code by email for a user who forgot their password. Behaves exactly like the portal reset: same code length, expiry, attempt limits, and send-rate limits. The response is always generic and never reveals whether an account exists.

POST /api/v1 action=resetpassword
ParameterTypeRequiredDescription
sessionidstringRequiredSession ID from init handshake
emailstringEitherEmail of the account to reset
usernamestringEitherUsername of the account to reset (provide email or username)

Success response

JSON
{ "success": true, "message": "password_reset_email_sent" }

Always returned whether or not the account exists (no user enumeration). A code email is only sent when the account is real. Limits (identical to portal): max 5 requests per 15 min per IP (30 min block), max 5 reset requests per account per 60 min, and a new code is blocked while the current one is still active (~20 min).

Error responses

400 / 404 / 429
JSON
{ "success": false, "message": "Too many password reset requests. Please try again later.", "error_code": "RESET_LIMIT_REACHED", "retry_after_minutes": 42 }

400 missing sessionid / no email or username (password_reset_identifier_required); 404 handshake not found; 429 per-IP throttle, account request limit (RESET_LIMIT_REACHED + retry_after_minutes), or a code still active (CODE_STILL_ACTIVE + retry_after).

Reset Password — Confirm

Submit the emailed code plus a new password to complete the reset. On success all of the user's existing sessions are revoked and they must log in again.

POST /api/v1 action=resetpasswordconfirm
ParameterTypeRequiredDescription
sessionidstringRequiredSession ID from init handshake
codestringRequiredThe reset code from the email
new_passwordstringRequiredThe new password to set
emailstringEitherEmail of the account (provide email or username)
usernamestringEitherUsername of the account

Success response

JSON
{ "success": true, "message": "password_reset_success", "sessions_revoked": 2 }

Error responses

400 / 410 / 423 / 429
JSON
{ "success": false, "message": "Invalid reset code.", "error_code": "CODE_MISMATCH", "attempts_remaining": 3 }

Wrong code CODE_MISMATCH (+ attempts_remaining); expired RESET_CODE_EXPIRED (410); max 5 attempts RESET_ATTEMPTS_EXCEEDED (423); weak password INVALID_PASSWORD; same as old PASSWORD_UNCHANGED; no pending reset NO_RESET_PENDING. Per-IP throttle: max 10 confirms per 15 min (15 min block) → 429.

Validate Access Key

Validate an issued access key and return its policy's subscription bundle. Send this action through the same encrypted application-API envelope established by init.

POST/api/v1action=accesskey
sessionidstringRequiredSession ID from init
keystringRequiredIssued access key
hwidstringPolicy-dependentHardware ID used for first-use bonding

Keys are issued through an IP-verified site challenge, then bind to the first device that validates them in an application. Validation does not compare the application's IP with the issuance IP. Success returns subscription_hashes, expires_at, binding_mode, and hwid_bound. Failure status values include invalid, expired, revoked, hwid_required, hwid_mismatch, used, and banned.

Get Global Variable

Retrieve a global application variable by ID or name. Supports access control: public, authenticated, role-based, and subscription-based. Public variables are cached for 30 seconds.

POST /api/v1 action=getglobalvariable
ParameterTypeRequiredDescription
varidstringRequiredVariable ID or name
sessionidstringRequiredSession ID from init

Success response

JSON
{ "success": true, "value": "variable_value" }

Error responses

400 Bad Request
JSON
{ "success": false, "message": "varID parameter is required" }

No variable identifier was provided in the request.

401 Unauthorized
JSON
{ "success": false, "message": "Authentication required" }

The variable requires authentication but no valid session was provided.

403 Forbidden
JSON
{ "success": false, "message": "Access denied" }

The customer's role is not in the variable's allowed roles list, or they do not have an active required subscription.

404 Not Found
JSON
{ "success": false, "message": "Global variable not found" }

The specified variable ID or name does not exist.

Get Data Variable

Retrieve a data variable with access control support (public, authenticated, role-based, subscription-based).

POST /api/v1 action=getdatavariable
ParameterTypeRequiredDescription
varidstringRequiredVariable ID or name
sessionidstringRequiredSession ID from init

Success response

JSON
{ "success": true, "value": "variable_value" }

Error responses

400 Bad Request
JSON
{ "success": false, "message": "varID parameter is required" }

No variable identifier was provided in the request.

401 Unauthorized
JSON
{ "success": false, "message": "Authentication required" }

The variable is not public and requires authentication, but no valid session was provided.

403 Forbidden
JSON
{ "success": false, "message": "You do not have the required role to access this variable" }

The customer's role is not in the variable's allowed roles list. Also returned when the required subscription is missing.

404 Not Found
JSON
{ "success": false, "message": "Data variable not found" }

The specified variable ID or name does not exist in the application's data variables.

Get User Variable

Retrieve a user-specific variable value for the authenticated customer.

POST /api/v1 action=getuservariable
ParameterTypeRequiredDescription
sessionidstringRequiredSession ID from init
varidstringRequiredVariable ID or name

Success response

JSON
{ "success": true, "value": "user_variable_value" }

Error responses

400 Bad Request
JSON
{ "success": false, "message": "varID parameter is required" }

No variable identifier was provided. Also returned when sessionid is missing.

404 Not Found
JSON
{ "success": false, "message": "User variable definition not found" }

The variable definition was not found by ID or name. Also returned when the session or customer is not found.

Get Shared User Variable

Retrieve shared user variable values across users, if sharing is enabled for the variable.

POST /api/v1 action=getuservariableshared
ParameterTypeRequiredDescription
sessionidstringRequiredSession ID from init
varidstringRequiredVariable ID or name
usernamestringOptionalFilter by specific user

Error responses

400 Bad Request
JSON
{ "success": false, "message": "varID parameter is required" }

No variable identifier was provided. Also returned when session credentials are missing.

403 Forbidden
JSON
{ "success": false, "message": "Variable is not share-enabled" }

The variable does not have sharing enabled. Also returned when the session is not authenticated.

404 Not Found
JSON
{ "success": false, "message": "User variable definition not found" }

The variable definition was not found. Also returned when the session or customer is not found.

Set User Variable

Set or update a user-specific variable value for the authenticated customer.

POST /api/v1 action=setuservariable
ParameterTypeRequiredDescription
sessionidstringRequiredSession ID from init
varidstringRequiredVariable ID or name
valueanyRequiredValue to set (string, number, object, array)

Success response

JSON
{ "success": true, "message": "variable_set_success" }

Error responses

400 Bad Request
JSON
{ "success": false, "message": "varID parameter is required" }

No variable identifier was provided. Also returned when the value parameter is missing or session credentials are absent.

404 Not Found
JSON
{ "success": false, "message": "User variable definition not found" }

The variable definition was not found by ID or name. Also returned when the session or customer is not found.

Get Subscriptions

Returns active subscription hashes for the authenticated customer.

POST /api/v1 action=getsubscriptions
ParameterTypeRequiredDescription
sessionidstringRequiredSession ID from init

Success response

JSON
{ "success": true, "subscriptions": [/* subscription hashes */] }

Error responses

400 Bad Request
JSON
{ "success": false, "message": "sessionid is required" }

No session ID was provided in the request.

403 Forbidden
JSON
{ "success": false, "message": "Session not authenticated" }

The handshake is not in an authenticated state.

404 Not Found
JSON
{ "success": false, "message": "Customer session missing" }

Session handshake or customer record not found for the provided session ID.

Download File

Download a file from the application's proxies. Supports streaming, encoding options, multipart downloads, and byte ranges.

POST /api/v1 action=downloadfile
ParameterTypeRequiredDescription
fileidstringRequiredFile ID or filename. Aliases: file_id, file, filename
sessionidstringRequiredSession ID from init
streambooleanOptionalSet true for raw binary stream (application/octet-stream). Much faster for large files.
encodingstringOptionalhex (default) or base64. Only applies in JSON mode (non-streaming).
multiintegerOptionalNumber of chunks (1–100) for multipart parallel download
startintegerOptionalStart byte for range request. Use with end.
endintegerOptionalEnd byte for range request. Use with start.

Success response (JSON mode)

JSON
{ "success": true, "encoding": "hex", "contents": "4a6f686e..." } // With base64 encoding { "success": true, "encoding": "base64", "contents": "Sm9obi4uLg==" } // Range request (206 Partial Content) { "success": true, "encoding": "hex", "contents": "4a6f...", "range": "bytes 0-1023", "partial_content": true }

When stream=true, the response is raw binary data with Content-Type: application/octet-stream instead of JSON.

Error responses

400 Bad Request
JSON
{ "success": false, "message": "fileid parameter is required" }

No file identifier was provided. Also returned for invalid byte range, invalid multi parameter (must be 1–100), or invalid integer values for range/multi.

403 Forbidden
JSON
{ "success": false, "message": "You do not have the required subscription to download this file." }

File requires a subscription or role the customer does not have.

404 Not Found
JSON
{ "success": false, "message": "File 'myfile' not found in proxies list." }

File ID does not match any proxy entry, or no file proxies are configured for the application. Also returned when the file entry has no URL or data content.

416 Range Not Satisfiable
JSON
{ "success": false, "message": "Requested range not satisfiable" }

The requested byte range exceeds the file's data size.

Get Download URL

Get the direct download URL for a file without streaming its content.

POST /api/v1 action=getdownloadurl
ParameterTypeRequiredDescription
fileidstringRequiredFile ID or filename
sessionidstringRequiredSession ID from init

Success response

JSON
{ "success": true, "url": "https://example.com/file.zip", "size": 12345 }

Error responses

400 Bad Request
JSON
{ "success": false, "message": "File 'myfile' contains inline data, not a URL. Use the downloadfile action instead." }

The file entry contains inline data rather than a URL. Also returned when the fileid parameter is missing.

403 Forbidden
JSON
{ "success": false, "message": "You do not have the required subscription to access this file URL." }

File requires a subscription or role the customer does not have.

404 Not Found
JSON
{ "success": false, "message": "File 'myfile' not found in proxies list." }

File ID does not match any proxy entry, or no file proxies are configured for the application.

Get File Hash

Get the SHA256 hash of a file for integrity verification without downloading the full file content.

POST /api/v1 action=getfilehash
ParameterTypeRequiredDescription
fileidstringRequiredFile ID, hash, or filename
sessionidstringRequiredSession ID from init

Success response

JSON
{ "success": true, "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "size": 12345 }

Error responses

403 Forbidden
JSON
{ "success": false, "message": "You do not have the required subscription to access this file." }

File requires a subscription or role the customer does not have.

404 Not Found
JSON
{ "success": false, "message": "File 'myfile' not found in proxies list." }

File ID does not match any proxy entry, or no file proxies are configured for the application.

Get Chat Messages

Retrieve the last 100 chat messages from a specific channel.

POST /api/v1 action=chatget
ParameterTypeRequiredDescription
sessionidstringRequiredSession ID from init
channelstringRequiredChannel/chatroom name

Success response

JSON
{ "success": true, "messages": [ { "id": 41, "message": "anyone got the new build?", "username": "kenny", "display_name": "Kenny", "timestamp": "2026-06-19T10:00:00Z", "customer_id": "101" // normal message — no reply_to }, { "id": 42, "message": "yeah it's in the portal", "username": "alice", "display_name": "Alice", "timestamp": "2026-06-19T10:01:00Z", "customer_id": "102", "reply_to": { // present → this message is a reply "id": 41, "author": "Kenny", "preview": "anyone got the new build?" } } ], "channel": "general" }

Each message that is a reply carries a reply_to object with the parent id, author, and a short content preview (frozen when the reply was sent, so it still renders even if the parent was later deleted). Normal messages omit this field — detect a reply by checking whether reply_to is present.

Error responses

400 Bad Request
JSON
{ "success": false, "message": "sessionid is required" }

No session ID was provided in the request.

403 Forbidden
JSON
{ "success": false, "message": "Session not authenticated" }

The handshake is not in an authenticated state.

404 Not Found
JSON
{ "success": false, "message": "Chat room does not exist." }

Session handshake not found, or the specified chat room/channel does not exist.

Send Chat Message

Send a message to a chat channel. Content is sanitized and filtered automatically.

POST /api/v1 action=chatsend
ParameterTypeRequiredDescription
sessionidstringRequiredSession ID from init
channelstringRequiredChannel/chatroom name
messagestringRequiredMessage content
reply_tointegerOptionalID of the message being replied to. Omit for a normal message. If the id is unknown or the parent was deleted, the reply link is ignored and the message posts normally. Aliases: parent_id, reply_id.

Success response

JSON
{ "success": true, "message": "message_sent_successfully", "message_data": { /* message object */ } }

When reply_to is supplied and the parent exists, the returned message_data includes a reply_to object with the parent id, author, and a short content preview (frozen at reply time so it still renders if the parent is later deleted).

Error responses

400 Bad Request
JSON
{ "success": false, "message": "Message parameter is required" }

Missing message content, message empty after sanitization, or message blocked by content filter (includes blocked: true and error_code: MESSAGE_BLOCKED).

403 Forbidden
JSON
{ "success": false, "message": "Your account is banned from chat." }

Customer account is banned. Also returned when the session is not authenticated.

404 Not Found
JSON
{ "success": false, "message": "Chat room does not exist." }

Session handshake, customer, or chat room not found.

Get Online Users

Fetch all currently online users based on recent heartbeat activity. Cached for 15 seconds.

POST /api/v1 action=getonlineusers
ParameterTypeRequiredDescription
sessionidstringRequiredSession ID from init

Success response

JSON
{ "success": true, "online_users": [ { "id": 123, "username": "user1" } ], "count": 1 }

App Status

Get application statistics including total users and online user count. Cached for 15 seconds.

POST /api/v1 action=getstatus
ParameterTypeRequiredDescription
sessionidstringRequiredSession ID from init

Success response

JSON
{ "success": true, "status": { "numUsers": 1000, "numOnlineUsers": 42 } }

Get Profile Picture

Get the authenticated customer's profile picture URL and username.

POST /api/v1 action=getprofilepicture
ParameterTypeRequiredDescription
sessionidstringRequiredSession ID from init

Success response

JSON
{ "success": true, "username": "user123", "profile_picture": "https://...", "has_profile_picture": true }

Error responses

400 Bad Request
JSON
{ "success": false, "message": "sessionid is required" }

No session ID was provided in the request.

403 Forbidden
JSON
{ "success": false, "message": "Session not authenticated" }

The handshake is not in an authenticated state.

404 Not Found
JSON
{ "success": false, "message": "Customer session missing" }

Session handshake or customer record not found.

Lookup Profile Picture

Look up another customer's profile picture by their username.

POST /api/v1 action=getprofilepicturebyusername
ParameterTypeRequiredDescription
sessionidstringRequiredSession ID from init
target_usernamestringRequiredUsername to look up

Success response

JSON
{ "success": true, "username": "target_user", "profile_picture": "https://...", "has_profile_picture": true, "is_self": false }

Error responses

400 Bad Request
JSON
{ "success": false, "message": "target_username parameter is required" }

Missing session ID or target username parameter.

403 Forbidden
JSON
{ "success": false, "message": "Session not authenticated" }

The handshake is not in an authenticated state.

404 Not Found
JSON
{ "success": false, "message": "Requested user could not be found" }

Session handshake not found, or the target username does not match any customer.

Check Blacklist

Check if the authenticated user is banned or blacklisted.

POST /api/v1 action=checkblacklist
ParameterTypeRequiredDescription
sessionidstringRequiredSession ID from init

Success response

JSON
{ "success": true, "blacklisted": false }

Error responses

400 Bad Request
JSON
{ "success": false, "message": "sessionid is required" }

No session ID was provided in the request.

403 Forbidden
JSON
{ "success": false, "message": "Session not authenticated" }

The handshake is not in an authenticated state or has no active session.

404 Not Found
JSON
{ "success": false, "message": "Customer session missing" }

Session handshake or customer record not found.

Ban

Bans the customer's account and all associated license keys. Intended to be triggered when anti-debug checks, integrity validation, or other detection mechanisms fail. This action is irreversible from the API.

POST /api/v1 action=ban
Destructive action. Permanently bans the account and all associated license keys. Typically called when anti-debug or integrity checks fail.
ParameterTypeRequiredDescription
sessionidstringRequiredSession ID from init
notestringOptionalBan reason/note

Success response

JSON
{ "success": true, "message": "account_banned_success", "keys_banned": 3 }

Error responses

400 Bad Request
JSON
{ "success": false, "message": "sessionid is required" }

Missing sessionid parameter.

403 Forbidden
JSON
{ "success": false, "message": "Session not authenticated" }

The handshake is not in an authenticated state.

404 Not Found
JSON
{ "success": false, "message": "Customer not found in customers list" }

Session handshake or customer record not found in the application's customer list.

Portal Login

Authenticate a customer and receive a session token for the portal API. Uses JSON request/response.

Portal API base URL: /api/portal/v1/{portal_key}/ — All portal endpoints require a valid portal key in the URL path. Auth endpoints use Bearer tokens in the Authorization header.
POST /api/portal/v1/{portal_key}/auth/login
ParameterTypeRequiredDescription
usernamestringRequiredCustomer username
passwordstringRequiredCustomer password

Success response

JSON
{ "success": true, "customer": { /* customer object */ }, "session": { "token": "hex_session_token", "expires_at": "2026-02-15T16:00:00Z" } }

Error responses

400 Bad Request
JSON
{ "success": false, "message": "Username and password are required" }

Missing credentials or non-JSON request body.

401 Unauthorized
JSON
{ "success": false, "message": "Invalid username or password" }

Wrong credentials. Also returned with error_code: EMAIL_NOT_VERIFIED when email verification is required.

403 Forbidden
JSON
{ "success": false, "message": "Access from your IP address has been blocked." }

IP blacklisted or account banned.

Portal Register

Create a new customer account with optional license key. Sends a verification email. Password must contain uppercase, lowercase, and digit (min 8 chars).

POST /api/portal/v1/{portal_key}/auth/register
ParameterTypeRequiredDescription
usernamestringRequired3-32 chars, alphanumeric/underscore/hyphen
emailstringRequiredValid email, max 254 chars
passwordstringRequiredMin 8 chars, must contain uppercase, lowercase, digit
keystringOptionalLicense key to redeem on registration
locationstringOptionalUser location, max 120 chars

Success response

JSON
{ "success": true, "message": "Verification code sent. Please confirm your email.", "customer": { /* customer object */ }, "verification_required": true, "verification_expires_at": "2026-02-15T16:10:00Z" }

Error responses

400 Bad Request
JSON
{ "success": false, "message": "Missing required field: username" }

Missing fields, invalid username/email/password format, duplicate username/email, invalid/used/upgrade-only key, or location too long.

403 Forbidden
JSON
{ "success": false, "message": "Access from your IP address has been blocked." }

IP blacklisted or registration key is banned.

Portal Verify Email

Verify email with a 6-digit code sent during registration.

POST /api/portal/v1/{portal_key}/auth/register/verify
ParameterTypeRequiredDescription
codestringRequired6-digit verification code from email
emailstringEitherCustomer email (provide email or username)
usernamestringEitherCustomer username (provide email or username)

Success response

JSON
{ "success": true, "message": "Email verified successfully", "verified": true }

Error responses

400 Bad Request
JSON
{ "success": false, "message": "Invalid or expired verification code", "error_code": "VERIFICATION_FAILED" }

Missing code/identifier, wrong code, expired code, or customer not found.

409 Conflict
JSON
{ "success": false, "message": "Email is already verified", "error_code": "ALREADY_VERIFIED" }

Email has already been verified.

423 Locked
JSON
{ "success": false, "message": "Maximum attempts exceeded. Please request a new code.", "error_code": "ATTEMPTS_EXCEEDED" }

Too many incorrect verification attempts.

Portal Resend Code

Resend the email verification code. Returns generic success even if account not found (prevents enumeration).

POST/api/portal/v1/{portal_key}/auth/register/resend
ParameterTypeRequiredDescription
emailstringEitherCustomer email
usernamestringEitherCustomer username

Success response

JSON
{ "success": true, "message": "Verification email resent", "resend_count": 1 }

Error responses

429 Too Many Requests
JSON
{ "success": false, "message": "A verification code is still active...", "error_code": "CODE_STILL_ACTIVE" }

Existing code not yet expired, or resend limit reached (RESEND_LIMIT_REACHED).

Password Reset

Two-step password reset: request a reset code via email, then confirm with the code and new password.

Request reset code

POST/api/portal/v1/{portal_key}/auth/password-reset/request
ParameterTypeRequiredDescription
emailstringEitherCustomer email
usernamestringEitherCustomer username
JSON
{ "success": true, "message": "If an account exists, a reset email has been sent." }

Confirm reset

POST/api/portal/v1/{portal_key}/auth/password-reset/confirm
ParameterTypeRequiredDescription
codestringRequiredReset code from email
new_passwordstringRequiredNew password (must meet complexity rules)
emailstringEitherCustomer email
usernamestringEitherCustomer username
JSON
{ "success": true, "message": "Password reset successfully", "sessions_revoked": 2 }

Session & Logout

Validate the current session or log out. Requires Authorization: Bearer {token} header.

Validate session

GET/api/portal/v1/{portal_key}/auth/session
JSON
{ "success": true, "valid": true, "customer": { /* customer with hwid_state, subscriptions */ }, "has_password": true, "session": { "expires_at": "..." } }

Logout

POST/api/portal/v1/{portal_key}/auth/logout
JSON
{ "success": true, "revoked": true }

Error responses

401 Unauthorized
JSON
{ "success": false, "valid": false, "message": "Session token required" }

Missing or invalid Bearer token, expired session, customer not found, account banned, or IP mismatch.

HWID Reset

Reset the customer's hardware ID. Requires authentication.

POST/api/portal/v1/{portal_key}/auth/hwid/reset
ParameterTypeRequiredDescription
notestringOptionalReason for reset

Success response

JSON
{ "success": true, "message": "HWID reset successful", "state": { /* hwid state */ }, "policy": { /* hwid policy */ } }

Error responses

403 Forbidden
JSON
{ "success": false, "message": "HWID resets are disabled for this application." }

HWID feature disabled or resets not available.

429 Too Many Requests
JSON
{ "success": false, "message": "HWID reset is on cooldown." }

Cooldown period has not elapsed since last reset.

Redeem Key

Redeem an additional license key on an existing account. Requires authentication.

POST/api/portal/v1/{portal_key}/auth/redeem-key
ParameterTypeRequiredDescription
keystringRequiredLicense key value (also accepts license_key)

Success response

JSON
{ "success": true, "message": "Key redeemed successfully", "customer": { /* updated customer */ } }

Error responses

400 Bad Request
JSON
{ "success": false, "message": "License key is required" }

Missing key, key too short, already redeemed, or upgrade key incompatible with current subscriptions.

404 Not Found
JSON
{ "success": false, "message": "License key not recognized" }

Key does not exist. Also returns 403 if the key is banned.

Set Password

Set a password for OAuth-only accounts that don't have one. Requires authentication.

POST/api/portal/v1/{portal_key}/auth/set-password
ParameterTypeRequiredDescription
passwordstringRequiredNew password (must meet complexity rules)

Success response

JSON
{ "success": true, "message": "Password set successfully" }

Error responses

400 Bad Request
JSON
{ "success": false, "message": "Password is already set. Use change-password instead." }

Account already has a password, password field is empty, or password does not meet complexity requirements.

Issue an Access Key

Build the key-generation interface on your own site and call Proudly's portal API from its JavaScript. The site's exact HTTPS origin must match the integration selected by the policy.

GET/api/portal/v1/{portal_key}/access-key-policies/{slug}
POST/api/portal/v1/{portal_key}/access-keys/challenge
POST/api/portal/v1/{portal_key}/access-keys/issue

Browser flow

  1. Fetch the policy metadata and read integration_id.
  2. Create a challenge with { "policy_slug": "your-slug", "integration_id": "..." }.
  3. If challenge.turnstile_required is true, render Turnstile with the returned site_key, action, and cdata. Otherwise continue immediately.
  4. Issue the key with { "challenge_id": "...", "turnstile_token": "..." }. Omit or leave the token empty when Turnstile is disabled.

GitHub Pages and other static hosts are supported. The Turnstile public site key may be used in browser JavaScript, but its secret must remain in Proudly. Configure the integration hostname in Cloudflare's Turnstile hostname allowlist.

Challenge creation and issuance accept only the configured integration origin. Issuance also verifies the request IP, five-minute single-use challenge, policy status, cooldown and bans. When Turnstile is enabled it additionally verifies the token hostname, action, and cData.

OAuth Flow

OAuth authentication flow for third-party providers (Discord, etc). Supports login, register, and account linking.

List providers

GET/api/portal/v1/{portal_key}/auth/oauth/providers
JSON
{ "success": true, "providers": [{ "name": "discord", "display_name": "Discord", "icon": "...", "color": "..." }] }

Authorize

GET/api/portal/v1/{portal_key}/auth/oauth/{provider}/authorize
ParameterTypeRequiredDescription
actionstringOptionallogin, register, or link (default: login)
redirect_uristringOptionalWhere to redirect after OAuth
session_tokenstringOptionalSession token for link action

Returns HTTP 302 redirect to the OAuth provider's authorization page.

Callback

GET/api/portal/v1/{portal_key}/auth/oauth/{provider}/callback

Handles the OAuth provider callback. Redirects to frontend with hash params: oauth_session (success), oauth_error (failure), or oauth_link_required (needs password confirmation).

Link account

POST/api/portal/v1/{portal_key}/auth/oauth/link
ParameterTypeRequiredDescription
link_tokenstringRequiredToken from OAuth callback
passwordstringRequiredCurrent account password

Link status

GET/api/portal/v1/{portal_key}/auth/oauth/status

Returns linked OAuth accounts, available providers, has_password, and can_unlink. Requires auth.

Unlink provider

POST/api/portal/v1/{portal_key}/auth/oauth/unlink
ParameterTypeRequiredDescription
providerstringRequiredProvider name to unlink (e.g. discord)

Error responses

400 Bad Request
JSON
{ "success": false, "message": "Invalid provider" }

Unknown or disabled provider, missing fields, invalid/expired link token, or cannot unlink last OAuth provider without a password set.

Portal Info & Stats

Get portal information, community statistics, subscription types, and badge definitions.

Portal info

GET/api/portal/v1/{portal_key}/info
JSON
{ "success": true, "portal": { "name": "Community", "banner": {}, "settings": {}, "oauth_providers": [] } }

Stats

GET/api/portal/v1/{portal_key}/stats
JSON
{ "success": true, "stats": { "total_members": 100, "online_members": 5, "total_threads": 50, "total_replies": 200 } }

Subscription types

GET/api/portal/v1/{portal_key}/subscriptions
JSON
{ "success": true, "subscriptions": [{ "public_id": "sub_abc", "name": "Premium", "display_name": "Premium Plan" }] }

My subscriptions

GET/api/portal/v1/{portal_key}/my-subscriptions

Requires authentication. Returns the logged-in user's active subscriptions with id, expires_at, and granted_at.

Badges

GET/api/portal/v1/{portal_key}/badges
JSON
{ "success": true, "badges": [{ "id": "badge_1", "name": "First Post", "icon": "...", "color": "#...", "type": "achievement" }], "badge_system_enabled": true }

Profile

Get and update customer profiles. View other users' public profiles.

Get own profile

GET/api/portal/v1/{portal_key}/profile

Requires authentication. Returns the full profile payload.

Update profile

PUT/api/portal/v1/{portal_key}/profile
ParameterTypeRequiredDescription
usernamestringOptional3-32 chars, alphanumeric/underscore/hyphen
locationstringOptionalMax 120 chars
statusstringOptionalMax 100 chars
biostringOptionalMax 500 chars
profile_picturestringOptionalURL, max 2000 chars

View user profile

GET/api/portal/v1/{portal_key}/profile/{user_id}

Requires authentication. Returns profile and friendship_status.

Lookup by username

GET/api/portal/v1/{portal_key}/users/by-username/{username}

No auth required (enhanced with auth). Returns profile and friendship_status. Returns 404 if user not found.

Members

Get a paginated list of members with online status. Requires search query (min 2 chars) unless requesting online-only users.

GET/api/portal/v1/{portal_key}/members
ParameterTypeRequiredDescription
qstringOptionalSearch query, min 2 chars (required unless online=true)
onlinestringOptionalSet true to show only online members
pageintegerOptionalPage number (default: 1)
limitintegerOptionalResults per page (10-50, default: 20)

Success response

JSON
{ "success": true, "members": [{ "id": 1, "username": "...", "is_online": true }], "total": 100, "online_count": 5, "has_more": true }

Search

Global search across threads, posts/replies, and users.

GET/api/portal/v1/{portal_key}/search
ParameterTypeRequiredDescription
qstringRequiredSearch term, min 2 chars
typestringOptionalthreads, posts, users, or all (default: all)
limitintegerOptionalMax results per type (default: 10, max: 50)
pageintegerOptionalPage number (default: 1, max: 100)

Success response

JSON
{ "success": true, "results": { "threads": [], "posts": [], "users": [] } }

Variables

Read global & data variables that have been flagged with Allow Portal Access in the dashboard. The variable's existing access control (public / authenticated / roles / subscriptions) is still enforced — pass a customer session as Authorization: Bearer <session_token> to read non-public variables.

List global variables

GET/api/portal/v1/{portal_key}/variables

Returns every portal-enabled global variable. value is included only when the caller is allowed to read it; otherwise accessible is false and the value is omitted.

JSON
{ "success": true, "variables": [ { "id": 123, "name": "welcome_message", "access_type": "public", "accessible": true, "value": "Hello!" }, { "id": 124, "name": "premium_config", "access_type": "subscriptions", "accessible": false } ] }

Get one global variable

GET/api/portal/v1/{portal_key}/variables/{name}

{name} is the variable name or id. Returns 404 if the variable does not exist or is not portal-enabled; 401/403 if access control is not satisfied.

JSON
{ "success": true, "value": "Hello!" }

List data variables

GET/api/portal/v1/{portal_key}/data-variables

Same shape as global variables, plus a schema field when one is defined.

Get one data variable

GET/api/portal/v1/{portal_key}/data-variables/{name}

Friends

Friend system — list friends, manage requests, remove friends. All endpoints require authentication.

List friends

GET/api/portal/v1/{portal_key}/friends

Pending requests

GET/api/portal/v1/{portal_key}/friends/requests

Returns incoming and outgoing request arrays.

Send / Accept / Reject / Cancel

POST/api/portal/v1/{portal_key}/friends/{action}

Actions: request, accept, reject, cancel. Body: {"user_id": 123}

Remove friend

DELETE/api/portal/v1/{portal_key}/friends/{user_id}

Direct Messages

Private messaging between users. All endpoints require authentication.

List conversations

GET/api/portal/v1/{portal_key}/messages/conversations
JSON
{ "success": true, "conversations": [{ "conversation_id": "123_456", "participant": {}, "last_message": {}, "unread_count": 2 }] }

Unread count

GET/api/portal/v1/{portal_key}/messages/unread-count

Get conversation

GET/api/portal/v1/{portal_key}/messages/conversations/{id}

Returns messages with sender info. Errors: 403 access denied, 404 not found.

Send message

POST/api/portal/v1/{portal_key}/messages
ParameterTypeRequiredDescription
recipient_idintegerEitherRecipient user ID
recipient_usernamestringEitherRecipient username
contentstringRequiredMessage content, min 2 chars

Mark as read

POST/api/portal/v1/{portal_key}/messages/conversations/{id}/read

Notifications

User notification management. All endpoints require authentication.

List notifications

GET/api/portal/v1/{portal_key}/notifications
ParameterTypeRequiredDescription
limitintegerOptionalMax notifications (default: 20, max: 50)
unread_onlystringOptionalSet true for unread only

Unread count

GET/api/portal/v1/{portal_key}/notifications/unread-count

Mark as read

POST/api/portal/v1/{portal_key}/notifications/{id}/read

Mark all as read

POST/api/portal/v1/{portal_key}/notifications/read-all

User Blocking

Block and unblock users. Requires authentication.

Block / Unblock

POST/api/portal/v1/{portal_key}/users/block
ParameterTypeRequiredDescription
user_idintegerRequiredTarget user ID
actionstringRequiredblock or unblock

Blocked list

GET/api/portal/v1/{portal_key}/users/blocked

Forum Categories

Browse, create, update, reorder, and delete forum categories. Admin role required for write operations.

List categories

GET/api/portal/v1/{portal_key}/forum/categories

No auth required (enhanced with auth for permissions). Returns categories with user_role and permissions.

Create category

POST/api/portal/v1/{portal_key}/forum/categories
ParameterTypeRequiredDescription
namestringRequiredCategory name, max 100 chars
descriptionstringOptionalCategory description
parent_idintegerOptionalParent category ID for subcategories

Reorder

POST/api/portal/v1/{portal_key}/forum/categories/reorder

Admin only. Body: {"positions": [...]}

Update category

PUT/api/portal/v1/{portal_key}/forum/categories/{id}

Delete category

DELETE/api/portal/v1/{portal_key}/forum/categories/{id}

Admin only. Cannot delete categories with subcategories or threads.

Forum Threads

Browse, create, view, edit, and delete forum threads. Supports polls and edit history.

List threads in category

GET/api/portal/v1/{portal_key}/forum/categories/{id}/threads

Query: page, limit (max 50). Returns threads, category, pagination, can_post.

Create thread

POST/api/portal/v1/{portal_key}/forum/categories/{id}/threads
ParameterTypeRequiredDescription
titlestringRequiredThread title, max 200 chars
contentstringRequiredThread body, max 50000 chars
pollobjectOptionalPoll: question, options (2-10), allow_multiple, ends_at

View thread

GET/api/portal/v1/{portal_key}/forum/threads/{id}

Returns full thread with replies, likes, reactions, poll, and edit info.

Edit thread

PUT/api/portal/v1/{portal_key}/forum/threads/{id}

Author or moderator. Body: title, content, edit_reason. Saves edit history.

Delete thread

DELETE/api/portal/v1/{portal_key}/forum/threads/{id}

Author or moderator. Deletes the thread and all its replies.

Recent activity

GET/api/portal/v1/{portal_key}/forum/recent-activity

Query: limit (max 20), type (all/threads/replies/members/badges). Returns mixed activity feed.

Forum Replies

Post, edit, and delete replies on threads. Supports nested replies and edit history.

List replies

GET/api/portal/v1/{portal_key}/forum/threads/{id}/replies

Query: page, limit (max 50). Returns replies and pagination.

Post reply

POST/api/portal/v1/{portal_key}/forum/threads/{id}/replies
ParameterTypeRequiredDescription
contentstringRequiredReply content, 3-10000 chars
parent_reply_idintegerOptionalParent reply ID for nested replies

Returns 403 if thread is locked. Creates notifications for thread author, parent reply author, and @mentions.

Edit reply

PUT/api/portal/v1/{portal_key}/forum/threads/{id}/replies/{reply_id}

Delete reply

DELETE/api/portal/v1/{portal_key}/forum/threads/{id}/replies/{reply_id}

Edit history

GET/api/portal/v1/{portal_key}/forum/threads/{id}/history
GET/api/portal/v1/{portal_key}/forum/threads/{id}/replies/{reply_id}/history

Returns history array and edit_count. Requires authentication.

Likes, Reactions & Watch

Like, react to, and watch forum content. All require authentication.

Like thread / reply

POST/api/portal/v1/{portal_key}/forum/threads/{id}/like
POST/api/portal/v1/{portal_key}/forum/threads/{id}/replies/{reply_id}/like

Toggle like. Returns liked (boolean) and like_count.

React to thread / reply

POST/api/portal/v1/{portal_key}/forum/threads/{id}/react
ParameterTypeRequiredDescription
reaction_typestringRequiredlike, love, helpful, insightful, funny

One reaction per user. Same reaction again removes it. Returns user_reaction, reactions map, total_reactions.

Watch thread

GET/api/portal/v1/{portal_key}/forum/threads/{id}/watchers

Query: active_minutes (max 30, default 5). Returns watchers and count.

POST/api/portal/v1/{portal_key}/forum/threads/{id}/watch
DELETE/api/portal/v1/{portal_key}/forum/threads/{id}/watch

Start/stop watching a thread (heartbeat for "who's reading").

Forum Moderation

Moderator actions for forum threads. Requires moderator role.

Lock / Unlock

POST/api/portal/v1/{portal_key}/forum/threads/{id}/lock

Toggles lock state. Returns is_locked.

Pin / Unpin

POST/api/portal/v1/{portal_key}/forum/threads/{id}/pin
ParameterTypeRequiredDescription
pin_typestringOptionalnone, pinned, or announcement. Omit to toggle.
pinned_untilstringOptionalISO datetime for auto-expiring pin

Move thread

POST/api/portal/v1/{portal_key}/forum/threads/{id}/move
ParameterTypeRequiredDescription
category_idintegerRequiredTarget category ID

Polls

View and vote on thread polls.

Get poll

GET/api/portal/v1/{portal_key}/forum/threads/{id}/poll
JSON
{ "success": true, "poll": { "question": "...", "options": [{ "index": 0, "text": "...", "votes": 5, "percentage": 50.0 }], "total_votes": 10, "user_votes": [0], "has_voted": true } }

Vote

POST/api/portal/v1/{portal_key}/forum/threads/{id}/poll/vote
ParameterTypeRequiredDescription
optionsarrayRequiredArray of option indices to vote for

Portal Chat

Global chat messaging with moderation support. All endpoints require authentication.

Get messages

GET/api/portal/v1/{portal_key}/chat/messages
ParameterTypeRequiredDescription
limitintegerOptionalMax messages (default: 50, max: 100)
before_idintegerOptionalLoad messages before this ID (pagination)

Success response

JSON
{ "success": true, "messages": [ { "id": 41, "sender_id": 101, "content": "anyone got the new build?", "created_at": "2026-06-19T10:00:00Z", "sender": { "display_name": "Kenny", "username": "kenny" } // normal message — no reply_to }, { "id": 42, "sender_id": 102, "content": "yeah it's in the portal", "created_at": "2026-06-19T10:01:00Z", "sender": { "display_name": "Alice", "username": "alice" }, "reply_to": { // present → this message is a reply "id": 41, "author": "Kenny", "preview": "anyone got the new build?" } } ], "room": { "id": "global", "name": "Global Chat", "topic": "" } }

Each message that is a reply carries a reply_to object with the parent id, author, and a short content preview (frozen when the reply was sent, so it still renders even if the parent was later deleted). Normal messages omit this field — detect a reply by checking whether reply_to is present.

Send message

POST/api/portal/v1/{portal_key}/chat/messages
ParameterTypeRequiredDescription
contentstringRequiredMessage content, max 2000 chars
parent_idintegerOptionalID of the message being replied to. Omit for a normal message. Unknown or deleted parent ids are ignored and the message posts normally. Alias: reply_to.

Delete message

DELETE/api/portal/v1/{portal_key}/chat/messages/{id}

Moderator only.

Mute / Unmute participant

POST/api/portal/v1/{portal_key}/chat/participants/{key}/mute
DELETE/api/portal/v1/{portal_key}/chat/participants/{key}/mute

Moderator only. POST accepts optional reason in body.

Muted list

GET/api/portal/v1/{portal_key}/chat/muted

Moderator only. Returns list of muted participants with details.

Support Tickets

Create, view, reply to, and manage support tickets. All endpoints require authentication. Staff can see all tickets and manage properties.

List tickets

GET/api/portal/v1/{portal_key}/support/tickets

Regular users see only their own tickets; staff sees all. Returns tickets, total, is_staff.

Create ticket

POST/api/portal/v1/{portal_key}/support/tickets
ParameterTypeRequiredDescription
subjectstringRequiredTicket subject
bodystringRequiredTicket message (also accepts message)
categorystringOptionalTicket category
prioritystringOptionalTicket priority

View ticket

GET/api/portal/v1/{portal_key}/support/tickets/{id}

Returns full ticket with message history. Marks as read for the viewer.

Reply to ticket

POST/api/portal/v1/{portal_key}/support/tickets/{id}/reply
ParameterTypeRequiredDescription
bodystringRequiredReply content (also accepts message)

Close ticket

POST/api/portal/v1/{portal_key}/support/tickets/{id}/close

Update ticket (staff)

PATCH/api/portal/v1/{portal_key}/support/tickets/{id}
ParameterTypeRequiredDescription
prioritystringOptionalNew priority
categorystringOptionalNew category
statusstringOptionalopen, waiting, closed, resolved
assigned_tostringOptionalAssign to staff member

Reopen ticket (staff)

POST/api/portal/v1/{portal_key}/support/tickets/{id}/reopen

Reports

Submit and manage content reports. Moderator role required for viewing/updating reports.

Submit report

POST/api/portal/v1/{portal_key}/reports
ParameterTypeRequiredDescription
content_typestringRequiredthread, reply, chat, or user
content_idstringRequiredID of reported content
reasonstringRequiredspam, harassment, inappropriate, misinformation, other
detailsstringOptionalAdditional details, max 1000 chars

List reports (moderator)

GET/api/portal/v1/{portal_key}/reports

Query: status, limit (max 100), page.

Update report (moderator)

PUT/api/portal/v1/{portal_key}/reports/{id}
ParameterTypeRequiredDescription
statusstringRequiredpending, reviewed, resolved, dismissed
resolution_notestringOptionalResolution note, max 500 chars

Verify License Key

Check whether a license key exists and whether it has been redeemed. Optionally returns detailed key info.

Admin API authentication: All Admin API endpoints require the X-API-Key header set to your Admin Key (pa_...). Alternatively, use Authorization: Bearer pa_....
GET POST /v1/verifykey
ParameterTypeRequiredDescription
keystringRequiredThe license key to verify
infobooleanOptionalIf true, returns additional key details (redeemed_by, subscription types, etc.)

Success response

JSON
{ "success": true, "valid": true, "redeemed": false, "info": { "id": 123456, "notes": "", "subscription_types": ["premium"], "created_at": "2026-01-01T00:00:00Z", "redeemed_at": null, "redeemed_by": null, "redeemed_by_username": null, "grant_duration_unit": "days", "grant_duration_value": 30 } }

Error responses

400 Bad Request

Missing required parameter: key.

401 Unauthorized

Missing or invalid API key.

Generate License Keys

Generate one or more license keys for the application. Supports custom patterns, subscription types, and duration settings.

POST /v1/keys/generate
ParameterTypeRequiredDescription
countintegerOptionalNumber of keys to generate (1–100, default: 1)
subscription_typesarrayOptionalList of subscription type names to grant on redemption
duration_typestringOptionaldays, hours, minutes, seconds, or lifetime (default: lifetime)
duration_valuenumberConditionalRequired when duration_type is not lifetime
patternstringOptionalKey pattern with * as placeholders (e.g. KEY-****-****)
key_casestringOptionaluppercase, lowercase, or mixed (default: mixed)
notesstringOptionalNotes to attach to the generated keys

Success response 201

JSON
{ "success": true, "message": "5 keys generated successfully", "keys": [ { "id": 987654321, "key": "KEY-A1B2-C3D4", "subscription_types": ["premium"], "grant_duration_unit": "days", "grant_duration_value": 30, "created_at": "2026-02-15T12:00:00Z" } ] }

Get User Info

Retrieve detailed information about a customer by username, including subscription status, ban state, HWID, and variables.

GET /v1/users/info
ParameterTypeRequiredDescription
usernamestringRequiredCustomer username (query parameter)

Success response

JSON
{ "success": true, "user": { "id": 1, "username": "johndoe", "email": "[email protected]", "created_at": "2026-01-01T00:00:00Z", "last_login": "2026-02-15T10:00:00Z", "status": "active", "banned": false, "subscription": { /* subscription object */ }, "hwid_state": { /* HWID details */ }, "variables": [] } }

Error responses

404 Not Found

Customer not found.

Get User Keys

Retrieve all license keys redeemed by a specific customer.

GET /v1/users/keys
ParameterTypeRequiredDescription
usernamestringRequiredCustomer username (query parameter)

Success response

JSON
{ "success": true, "username": "johndoe", "keys": [ { "id": 1, "key": "KEY-XXXX-XXXX", "subscription_types": ["premium"], "created_at": "2026-01-01T00:00:00Z", "redeemed_at": "2026-01-02T00:00:00Z" } ], "total": 1 }

Get All User Variables

Retrieve user variables for every customer in the application. Returns each customer with their effective variable values (customer-specific or default).

GET /v1/users/variables

No parameters required beyond authentication.

Success response

JSON
{ "success": true, "customers": [ { "customer_id": 1, "username": "johndoe", "email": "[email protected]", "variables": [ { "id": "abc123", "name": "score", "value": "100", "default_value": "0" } ] } ], "total_customers": 1, "variable_definitions_count": 1 }

Set Global Variable

Replace the value of a global variable, identified by name or id.

POST /v1/variables/set
ParameterTypeRequiredDescription
namestringRequiredGlobal variable name or id
valuestringRequiredNew value (objects/arrays are JSON-serialized)

Success response

JSON
{ "success": true, "message": "Variable updated successfully", "variable": { "id": 123, "name": "welcome_message", "value": "Hi there", "updated_at": "2026-06-23T12:00:00" } }

Errors: 404 if the variable is not found.

Set User Variable

Directly set a specific customer's user variable value (admin override). The variable must already exist as a user-variable definition. If the customer has no value yet for that definition, one is created.

POST /v1/users/variables/set
ParameterTypeRequiredDescription
usernamestringRequiredCustomer username
namestringRequiredUser-variable definition name or id
valuestringRequiredNew value (objects/arrays are JSON-serialized)

Success response

JSON
{ "success": true, "message": "User variable updated successfully", "customer_id": 1, "username": "johndoe", "variable": { "id": "abc123", "name": "score", "value": "500" } }

Errors: 404 if the customer or the user-variable definition is not found.

Set Data Variable

Replace the entire value of a data variable, identified by name or id. Maximum value size is 10MB.

POST /v1/datavariables/set
ParameterTypeRequiredDescription
namestringRequiredData variable name or id
valuestringRequiredNew value (objects/arrays are JSON-serialized)

Success response

JSON
{ "success": true, "message": "Data variable updated successfully", "variable": { "id": 456, "name": "download_maps", "value": "{...}", "updated_at": "2026-06-23T12:00:00" } }

Errors: 404 if the data variable is not found; 400 if the value exceeds 10MB.

Ban User

Ban a customer by username. Banned users cannot log in or use the application.

POST /v1/users/ban
ParameterTypeRequiredDescription
usernamestringRequiredCustomer username
reasonstringOptionalBan reason

Success response

JSON
{ "success": true, "message": "User banned successfully.", "customer": { "id": 1, "username": "johndoe", "banned": true, "ban_reason": "Violation of terms" } }

Unban User

Remove a ban from a customer by username.

POST /v1/users/unban
ParameterTypeRequiredDescription
usernamestringRequiredCustomer username

Success response

JSON
{ "success": true, "message": "User unbanned successfully.", "customer": { "id": 1, "username": "johndoe", "banned": false } }

Reset User Password

Reset a customer's password. The new password must be at least 6 characters.

POST /v1/users/resetpassword
ParameterTypeRequiredDescription
usernamestringRequiredCustomer username
new_passwordstringRequiredNew password (min 6 characters)

Success response

JSON
{ "success": true, "message": "Password reset successful.", "customer": { "id": 1, "username": "johndoe" } }

Reset User HWID

Reset the hardware ID (HWID) for a specific customer, allowing them to log in from a new device.

POST /v1/users/resethwid
ParameterTypeRequiredDescription
usernamestringRequiredCustomer username

Success response

JSON
{ "success": true, "message": "HWID reset successful.", "customer": { "id": 1, "username": "johndoe" }, "hwid_state": { /* updated HWID details */ } }

Reset All HWIDs

Reset hardware IDs for all customers in the application. Only resets customers who currently have an HWID set.

POST /v1/users/resethwid/all

No parameters required beyond authentication.

Success response

JSON
{ "success": true, "message": "HWID reset for 42 customers.", "reset_count": 42, "total_customers": 100 }

Get Subscription Types

Retrieve all subscription types defined for the application.

GET /v1/subscriptions

No parameters required beyond authentication.

Success response

JSON
{ "success": true, "subscriptions": [ { "name": "premium", "id": 1 }, { "name": "vip", "id": 2 } ], "total": 2 }

Extend User Subscription

Grant or extend a subscription for a customer. If the customer already has the subscription, the duration is added to the existing expiry.

POST /v1/users/subscription/extend
ParameterTypeRequiredDescription
usernamestringRequiredCustomer username
subscriptionstringRequiredSubscription name to grant/extend
duration_typestringOptionaldays, hours, minutes, seconds, or lifetime (default: days)
duration_valuenumberConditionalRequired when duration_type is not lifetime

Success response

JSON
{ "success": true, "message": "Subscription extended successfully.", "customer": { "id": 1, "username": "johndoe", "subscription_types": ["premium"] }, "grant": { /* grant record details */ } }

Validate Access Key

Validate an access key from a trusted server using X-API-Key and Idempotency-Key.

POST/api/key/v1/access-keys/validate

Body fields are key, trusted end-user ip, and hwid. Success returns the authorized subscription_hashes.