No matching endpoints
Nothing here matches
Send your app's X-App-Secret header and POST JSON to /api/auth/* to validate keys and run sessions. Generate keys from /api/admin/* with a Bearer token. Responses are always { "success": true|false, … }.
Introduction
The PWF Auth API is organised around REST. All requests and responses are JSON over HTTPS. There are three families of endpoints:
- Client API (
/api/auth/*,/api/update/*) — called from your shipped application using your app's secret. - Admin API (
/api/admin/*) — key & app management, authenticated with a Bearer token. - Customer / reseller API (
/api/customer/*,/api/reseller/*) — self-service portals.
All paths below are relative to that base URL. Every endpoint requires HTTPS; plain-HTTP requests are redirected.
Prefer machines? The whole surface is described in openapi.json (OpenAPI 3.1) — import it into Postman, Insomnia, or a code generator.
API and platform changes are announced in the changelog — breaking changes are always flagged there first.
Authentication
Client endpoints — App Secret
Send your app's secret in the X-App-Secret header. You'll find it in Dashboard → your app. The secret identifies which application a request belongs to.
curl https://pwfauth.com/api/auth/check-key.php \
-H "X-App-Secret: 9f02532d9c60… # 64-char hex, from your dashboard" \
-H "Content-Type: application/json" \
-d '{"license_key":"XXXXX-XXXXX-XXXXX-XXXXX"}'
Admin endpoints — Bearer token
Authenticate to /api/admin/login.php to receive a JWT, then send it as Authorization: Bearer <token> on every admin call.
TOKEN=$(curl -s https://pwfauth.com/api/admin/login.php \
-d '{"username":"admin","password":"…"}' | jq -r .token)
curl https://pwfauth.com/api/admin/keys.php \
-H "Authorization: Bearer $TOKEN"
The encrypted envelope
SDK-grade endpoints exchange an AES-256-CBC + HMAC-SHA256 envelope instead of plain JSON. The wire format is {"p": base64(IV || ciphertext), "t": unix_timestamp, "s": hmac_sha256_hex(p + t)} — encryption and MAC keys are both derived from your app secret, so no extra key exchange is needed.
{
"p": "aXYxNmJ5dGVzY2lwaGVydGV4dA…", // base64( IV || AES-256-CBC ciphertext )
"t": 1785671000, // unix timestamp (±300s accepted)
"s": "b7fd09…" // hmac_sha256_hex( p + t, mac_key )
}
Endpoints marked App secret + encrypted envelope require it for the request and reply encrypted. check-key.php auto-detects: send plain JSON and you get plain JSON back (that is what makes the curl examples on this page work); send an envelope and the reply is encrypted.
Envelopes older than ±300 seconds are rejected, so keep the client clock reasonably accurate. You never need to hand-roll this: the open-source example clients (VB.NET, C#, Python) ship a ready CryptoEnvelope implementation.
How the envelope is built
Both directions use the same recipe. Two keys are derived from your app secret, so every app gets distinct encryption and signing keys:
enc_key = SHA256("enc:" + app_secret) # 32 bytes
mac_key = SHA256("mac:" + app_secret) # 32 bytes
# client → server (and server → client — same recipe)
iv = random_bytes(16)
ct = AES-256-CBC(enc_key, iv, json_body) # PKCS#7 padding
p = base64(iv || ct)
t = unix_time() # ±300 s accepted
s = hex(HMAC-SHA256(p + str(t), mac_key))
send { "p": p, "t": t, "s": s }
# receiving: verify s FIRST (constant-time), check |now - t| ≤ 300,
# then base64-decode p, split off the 16-byte IV, decrypt the rest.
Which endpoints require it
Each endpoint card carries a colour-coded auth pill; this table is the same information at a glance:
| Auth model | Endpoints |
|---|---|
| Public | app/changelog · app/pricing · admin/login |
| App secret · plain or envelope | auth/check-key |
| App Secret | auth/trial · auth/request-hwid-reset · auth/account-register · auth/account-login · auth/change-password · update/validate · update/download |
| App secret + encrypted envelope | auth/login · auth/heartbeat · auth/logout · app/text · app/slides · app/info · app/social-click · update/check |
| Bearer | admin/* |
| Fulfillment secret | app/fulfill |
Responses & errors
Every response carries a boolean success. Errors add a human message and a stable, machine-readable error_code:
{
"success": false,
"message": "This license key has been banned.",
"error_code": "BANNED"
}
| error_code | Meaning |
|---|---|
MISSING_FIELDS | A required body field is absent. |
INVALID_KEY | The license key does not exist for this app. |
INVALID_CREDENTIALS | Username/password pair rejected (user accounts + admin login). |
HWID_MISMATCH | The key is bound to another device. |
EXPIRED | The license has passed its expiry date. |
BANNED / PAUSED | The key was banned or paused by the owner. |
MAINTENANCE | The app is in maintenance mode. |
SESSION_EXPIRED / SESSION_MISMATCH | The heartbeat session is invalid. |
TOO_MANY_ATTEMPTS | Login brute-force lockout — the account is locked temporarily; retry_after in the response says for how many seconds. |
TRIAL_DISABLED / TRIAL_USED / TRIAL_LIMIT | Trial creation refused — disabled for this app, already used on this device, or the per-IP cap was reached. |
HTTP status codes
| error_code | Meaning |
|---|---|
200 | Success — the JSON body carries the result. |
400 | Validation failed — missing or malformed fields (see error_code). |
401 | Missing or invalid X-App-Secret / Bearer token. |
403 | Authenticated but not allowed (wrong app, revoked token…). |
404 | Resource not found. |
405 | Wrong HTTP method for this endpoint. |
429 | Rate limited — wait for the Retry-After header before retrying. |
500 | Server error — generic message on purpose; retry later. |
Rate limits & fair use
A general per-IP request limiter protects every endpoint — flood it and you get 429 with a Retry-After header (authenticated admins get a higher ceiling). On top of that, a few targeted guards:
- Admin login is brute-force protected — repeated failures lock the account temporarily.
- Trials are capped per device (HWID) and per IP; exceeding them returns
TRIAL_LIMIT. - Secret regeneration is rate-limited per app to stop accidental rotation loops.
- Keep heartbeats at the interval the login response gives you (default 30s) — hammering faster adds nothing.
Quickstart
The minimal integration: validate a key when your app launches, then heartbeat to keep the session alive.
# Validate a key from the terminal (plain JSON works here)
curl -X POST https://pwfauth.com/api/auth/check-key.php \
-H "X-App-Secret: $APP_SECRET" \
-d '{"license_key":"XXXXX-XXXXX-XXXXX-XXXXX"}'
# → { "success": true, "valid": true, "key": { "status": "active", … } }
Note: the full session flow (login → heartbeat → logout) uses the encrypted envelope, so it is not hand-curl-able. Wire it in minutes with the in-panel Quick Start (generates snippets in 9 languages with your real credentials) or the open-source example clients.
The same flow with an official SDK
Login, heartbeat and the server-side kill switch in a few lines — the SDK handles the encrypted envelope for you:
// dotnet add package PWFAuth
using PWFAuth;
var client = new PwfClient(APP_SECRET);
client.SessionEnded += (s, e) => // ban / pause / expiry / revoke / offline
{
Console.WriteLine($"{e.ErrorCode}: {e.Message}");
Environment.Exit(0);
};
var login = await client.LoginAsync("XXXXX-XXXXX-XXXXX-XXXXX");
if (!login.Success) { Console.WriteLine(login.Message); return; }
client.StartHeartbeat(); // keeps the session alive AND enforces the kill switch
' dotnet add package PWFAuth
Dim client As New PwfClient(APP_SECRET)
AddHandler client.SessionEnded, Sub(s, e) ' ban / pause / expiry / revoke / offline
MessageBox.Show(e.Message)
Application.Exit()
End Sub
Dim login = Await client.LoginAsync("XXXXX-XXXXX-XXXXX-XXXXX")
If login.Success Then client.StartHeartbeat()
# pip install pwfauth
import sys
from pwfauth import PwfClient
client = PwfClient(APP_SECRET)
client.on_session_ended = lambda code, msg: sys.exit(msg) # kill switch
login = client.login("XXXXX-XXXXX-XXXXX-XXXXX")
if not login.success:
sys.exit(login.message)
client.start_heartbeat() # keeps the session alive AND enforces the kill switch
// npm install pwfauth
import { PwfClient } from 'pwfauth';
const client = new PwfClient(process.env.PWFAUTH_SECRET);
client.on('sessionEnded', ({ errorCode, message }) => { // kill switch
console.error(`${errorCode}: ${message}`);
process.exit(1);
});
const login = await client.login('XXXXX-XXXXX-XXXXX-XXXXX');
if (!login.success) { console.error(login.message); process.exit(1); }
client.startHeartbeat(); // keeps the session alive AND enforces the kill switch
Licenses & sessions
Activate a key on first use, bind it to the device's HWID, and open a session. Re-validates and starts a new session on subsequent logins. Enforces max_devices by retiring the oldest session.
| Body field | Req. | Description |
|---|---|---|
license_key | required | The license key to activate / validate. |
hwid | required | Stable hardware fingerprint of the device. |
{
"success": true,
"session_id": "8f2c…",
"user": {
"license_key": "XXXXX-XXXXX-XXXXX-XXXXX",
"key_type": "days", "duration": 30,
"expires_at": "2026-09-01T12:00:00Z",
"days_remaining": 30, "status": "active"
},
"features": { "pro_tier": true },
"heartbeat_interval": 30
}
Keep a session marked active. Call it every heartbeat_interval seconds (returned by login). Stop calling it and the session is reaped server-side.
| Body field | Req. | Description |
|---|---|---|
session_id | required | Session returned by login. |
license_key | optional | If sent, the session must belong to this key. |
{ "success": true, "message": "Heartbeat received" }
// the moment the key is banned / paused / expired / reset, the SAME call answers:
{ "success": false, "error_code": "BANNED",
"message": "This license key has been banned." } // session is dropped — log the user out
Close a session immediately (e.g. on app exit). Body: session_id required, license_key optional.
Lightweight status lookup — no session created, no HWID binding. Useful for a "is this key still valid?" check.
| Body field | Req. | Description |
|---|---|---|
license_key | required | The key to inspect. |
curl -X POST https://pwfauth.com/api/auth/check-key.php \
-H "X-App-Secret: $APP_SECRET" \
-d '{"license_key":"XXXXX-XXXXX-XXXXX-XXXXX"}'
var http = new HttpClient();
http.DefaultRequestHeaders.Add("X-App-Secret", APP_SECRET);
var res = await http.PostAsync("https://pwfauth.com/api/auth/check-key.php",
new StringContent("{\"license_key\":\"XXXXX-XXXXX-XXXXX-XXXXX\"}"));
import urllib.request, json
req = urllib.request.Request("https://pwfauth.com/api/auth/check-key.php",
data=json.dumps({"license_key": "XXXXX-XXXXX-XXXXX-XXXXX"}).encode(),
headers={"X-App-Secret": APP_SECRET})
print(json.load(urllib.request.urlopen(req)))
const res = await fetch('https://pwfauth.com/api/auth/check-key.php', {
method: 'POST',
headers: { 'X-App-Secret': process.env.APP_SECRET },
body: JSON.stringify({ license_key: 'XXXXX-XXXXX-XXXXX-XXXXX' })
});
console.log(await res.json());
$ch = curl_init('https://pwfauth.com/api/auth/check-key.php');
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['X-App-Secret: ' . $APP_SECRET],
CURLOPT_POSTFIELDS => '{"license_key":"XXXXX-XXXXX-XXXXX-XXXXX"}']);
$res = json_decode(curl_exec($ch), true);
{
"success": true, "valid": true,
"key": {
"status": "active", "key_type": "days", "duration": 30,
"activated_at": "2026-08-02T12:00:00Z",
"expires_at": "2026-09-01T12:00:00Z",
"days_remaining": 30, "max_devices": 1
}
}
Lightweight license validation without HWID binding or a session — ideal for a pre-flight check before the full login. Plain JSON in, plain JSON out.
| Body field | Req. | Description |
|---|---|---|
license_key | required | The key to inspect. |
Submit a HWID-reset request from your in-app dialog when a user changes machines. The app owner approves it from the dashboard.
| Body field | Req. | Description |
|---|---|---|
license_key or username | required | Identifies whose binding to reset. |
reason | optional | Note shown to the owner. |
Free Trials
Issue a time-limited trial key bound to the requesting device. One trial per HWID per app. Trial duration is configured by the app owner.
| Body field | Req. | Description |
|---|---|---|
hwid | required | Device fingerprint requesting the trial. |
{
"success": true, "message": "Trial activated! You have 3 days.",
"trial_key": "TRIAL-1A2B3-C4D5E-F6A7B",
"session_id": "8f2c…",
"expires_at": "2026-08-23T12:00:00Z", "days_remaining": 3,
"user": { "key_type": "days", "status": "active", … },
"app": { "name": "…", "version": "…" }
}
User accounts
Prefer username/password logins over raw keys? PWF Auth ships a full account model with bcrypt hashing and HWID binding.
Create an end-user account. Body: username, password required, email optional.
| Body field | Req. | Description |
|---|---|---|
username | required | Desired username (unique per app). |
password | required | Password (stored hashed server-side). |
email | optional | Contact e-mail for the account. |
Authenticate and bind a device. Body: username, password, hwid required. Returns the same session shape as login.
| Body field | Req. | Description |
|---|---|---|
username | required | Account username. |
password | required | Account password. |
hwid | required | Device fingerprint to bind the session to. |
Rotate a user's password. Body: username, current_password, new_password required.
| Body field | Req. | Description |
|---|---|---|
username | required | Account username. |
current_password | required | Current password (verified before the change). |
new_password | required | New password. |
App Content & Remote Control
These endpoints let your shipped app pull content you edit live in the panel — texts, slides, pricing, changelog — without releasing an update.
Fetch remote text(s). With ?name=… returns one entry; without it returns every text of the app. Per-key overrides win over app defaults. The license key rides in Authorization: Bearer
| Body field | Req. | Description |
|---|---|---|
?name | optional | Text key to fetch (omit for all). |
Returns the app's active announcement slides. Send the envelope body {"action":"get_slides"}; the reply is encrypted.
App metadata + social links (name, branding, URLs). Enveloped response.
Increment the click counter of a social link (for the panel's engagement stats).
| Body field | Req. | Description |
|---|---|---|
link_id | required | Numeric id of the social link. |
Public, no-auth changelog feed for one app — render a "What's new" widget anywhere. Published entries only.
| Body field | Req. | Description |
|---|---|---|
?app_id | required | The app whose changelog to read. |
?limit | optional | 1–50, default 20. |
?since | optional | ISO date — only entries published on/after. |
?category | optional | Filter: new / improved / fixed / removed / security / deprecated. |
Public pricing surface: subscription levels and legacy plans for an app; POST creates a purchase order; ?action=check reads a user's active subscription.
| Body field | Req. | Description |
|---|---|---|
?app_id | required | The app whose pricing to read. |
?type | optional | levels or plans (omit for both). |
Server-to-server order fulfillment for automated payment gateways — a verified IPN completes the order and triggers key generation + delivery. Authenticated by the fulfillment shared secret from your panel settings, never by a user session.
| Body field | Req. | Description |
|---|---|---|
order_id | required | The order to fulfill (ORD-…). |
secret | required | Your order_fulfill_secret. |
payment_ref | optional | Optional gateway payment reference. |
OTA updates
Ask whether a newer build is available for the caller — honouring channel, OS/arch filters, minimum-version gates, and staged rollouts (the same caller is stably bucketed).
| Body field | Req. | Description |
|---|---|---|
v | required | Caller's current version (semver). |
channel | optional | stable (default), beta, or alpha. |
os / arch | optional | windows·macos·linux / x64·x86·arm64. |
hwid / license_key | optional | Used for stable rollout bucketing. |
{
"success": true, "update_available": true,
"update": {
"version": "1.4.0", "channel": "stable",
"is_mandatory": false, "file_size": 5242880,
"sha256": "…", "changelog": "…",
"download_url": "api/update/download.php?v=1.4.0"
}
}
Stream the binary for a given version. Use the download_url returned by check.php, and verify the sha256 after download.
Webhooks
Turn events into HTTP calls to your server: key activations, bans, expiries, orders, new updates and more. Configure destinations per app (or for all apps) in the panel under Application → Webhooks, pick the events you care about, and PWF Auth POSTs a signed JSON body to your URL. Delivery is queue-based — expect it within a minute or two of the event, not in the same instant.
Delivery payload
{
"event": "key.activated",
"timestamp": "2026-08-20T14:07:02Z",
"data": {
"app_id": "01af6643-…",
"license_key": "XXXXX-XXXXX-XXXXX-XXXXX",
"hwid": "A1B2C3…"
}
}
Delivery headers
| Header | Description |
|---|---|
X-Webhook-Id | Unique delivery id (whd_…). Stays the same across retries of one event — use it to deduplicate. |
X-Webhook-Timestamp | Unix timestamp of the delivery attempt. |
X-Webhook-Attempt | 1-based attempt counter. |
X-Webhook-Signature | Legacy signature: hex(HMAC-SHA256(body, secret)). |
X-Webhook-Signature-V2 | Preferred signature: t=<ts>,v1=hex(HMAC-SHA256(ts + "." + body, secret)) — the timestamp inside the MAC makes replays detectable. |
Verifying the signature
Always verify before trusting a delivery: compute the V2 MAC over timestamp + "." + raw body with your endpoint secret, compare in constant time, and reject stale timestamps (±5 minutes is plenty). Answer 2xx fast — heavy work belongs in your own queue.
// X-Webhook-Signature-V2: t=<ts>,v1=<hex>
[$t, $v1] = explode(',', $_SERVER['HTTP_X_WEBHOOK_SIGNATURE_V2'] ?? ',');
$t = substr($t, 2); $v1 = substr($v1, 3);
$raw = file_get_contents('php://input');
$ok = hash_equals(hash_hmac('sha256', $t . '.' . $raw, $secret), $v1)
&& abs(time() - (int)$t) < 300;
if (!$ok) { http_response_code(400); exit; }
http_response_code(200); // answer fast — queue heavy work
// X-Webhook-Signature-V2: t=<ts>,v1=<hex>
const [t, v1] = req.headers['x-webhook-signature-v2']
.split(',').map(kv => kv.split('=')[1]);
const mac = crypto.createHmac('sha256', secret)
.update(`${t}.${rawBody}`).digest('hex');
const ok = crypto.timingSafeEqual(Buffer.from(mac), Buffer.from(v1))
&& Math.abs(Date.now() / 1000 - Number(t)) < 300;
res.sendStatus(ok ? 200 : 400); // answer fast — queue heavy work
# X-Webhook-Signature-V2: t=<ts>,v1=<hex>
import hmac, time
t, v1 = (kv.split("=", 1)[1]
for kv in request.headers["X-Webhook-Signature-V2"].split(","))
mac = hmac.new(secret, f"{t}.{raw_body}".encode(), "sha256").hexdigest()
ok = hmac.compare_digest(mac, v1) and abs(time.time() - int(t)) < 300
return ("", 200) if ok else ("", 400) # answer fast — queue heavy work
Retries & failure policy
- A delivery counts as successful on any
2xx/3xxanswered within 5 seconds. - Failures retry up to 5 attempts with widening gaps: 1 m → 5 m → 30 m → 2 h → 6 h.
X-Webhook-Idis minted once per event and reused on every retry — idempotency is oneseen?lookup away.- A destination that fails 20 times in a row is paused automatically; re-enable it from the panel (a
testevent button is right there).
Events
Subscribe to specific events or leave the filter empty to receive them all. event in the body tells you which one fired:
| Event | Fires when |
|---|---|
key.created | Keys are generated (panel or admin API). |
key.activated | A key's first successful login binds it to a device. |
key.banned / key.unbanned | A key is banned / unbanned. |
key.deleted | A key is deleted. |
key.rotated | A key string is rotated (the old string stops working). |
key.suspected_leak | Leak detection flags a key as suspected shared/leaked. |
key_expiring | A key approaches expiry (scheduled sweep). |
key_expired | A key expires (scheduled sweep). |
trial.created | A trial key is issued to a new device. |
hwid_reset.requested | A client requests a device (HWID) reset. |
new_order | A purchase order is created. |
order.approved | An order is approved and fulfilled. |
subscription.* | Subscription lifecycle: created / renewed / cancelled / renewal order created. |
update.published | A new application version is published. |
changelog.published | A changelog entry goes live. |
app.revoked | An application is revoked / disabled. |
suspicious_activity | The suspicious-activity heuristic fires. |
test | Manual test delivery from the panel. |
Admin API
Server-side endpoints for managing apps and keys. All require an Authorization: Bearer token from the login endpoint below.
Prefer not to store your password in a script? Mint a Personal Access Token in Panel → Profile → API Tokens and use it as the Bearer value (pwf_…). It never expires until you revoke it, works on every /api/admin/* endpoint, and is locked out of credential management (password, e-mail, 2FA, minting more tokens) by design.
Exchange credentials for a JWT. If 2FA is enabled, returns requires_2fa + a step1_token to complete via /api/admin/totp.php.
| Body field | Req. | Description |
|---|---|---|
username | required | Admin username. |
password | required | Admin password. |
Generate license keys in bulk. The same endpoint also supports GET (list), PUT (edit), DELETE, and action queries such as ?action=ban|pause|extend|reset-hwid and ?action=export.
curl -X POST https://pwfauth.com/api/admin/keys.php \
-H "Authorization: Bearer $TOKEN" \
-d '{"app_id":"01af6643-cf3a-4280-b9ce-cb5fa8ae987c",
"count":100,"key_type":"days","duration_value":30}'
The admin surface also covers apps, sessions, updates, feature-flags, webhooks, analytics, audit, resellers, and more — each under /api/admin/ with the same Bearer auth. Sign in to the dashboard to explore them.
SDKs
The REST API works from anything that can send an HTTP request. Official packages are live on the public registries — PWFAuth on NuGet (C# / VB.NET), pwfauth on npm (JavaScript / Node.js) and on PyPI (Python) — plus a VS Code extension and first-class PHP examples. Every SDK wires the encrypted envelope, the heartbeat and the kill switch for you.
- Open-source example clients (VB.NET · C# · Python) — full login/heartbeat/envelope flows, ready to copy
- In-panel Quick Start — 5 guided steps that generate working snippets in 9 languages with your real credentials
- OpenAPI 3.1 spec (
openapi.json) — one-click import into Postman / Insomnia, or feed it to your code generator
Ready to integrate?
Create a free account, register an app, and copy your X-App-Secret from the dashboard.