Manage Keys
Every license on your account, in one flat table. Use this panel to search, filter, act on individual keys, and run bulk operations across applications. Key generation gets its own route further down.
Live updates
The Live chip in the top right turns green when the SSE stream is connected. If it drops, the table falls back to polling.
Any of these events refresh the table in place without a full reload: license.generate, license.ban, license.delete, license.extend, license.bulk_extend, license.reset_hwid, client.license, client.register.
Counts strip
Chips at the top summarize the whole key set before any filter is applied. total counts every key, active covers used keys still inside their duration window, banned covers blocked ones, unactivated covers keys that were never redeemed, and expired covers redeemed keys whose window has passed.
Status is computed in the browser. A key counts as expired only when it has an expires_at in the past and is not lifetime. Lifetime keys and keys with duration_days === 0 never expire regardless of anything else.
Filters
Three filters stack above the table and compose before sorting is applied.
The search box does a case-insensitive substring match against key, used_by, and hwid. The app select is either All apps or one specific application by name. The status select is All statuses, Active, Banned, Unactivated, or Expired.
Columns
Every column is sortable. Click a header once for ascending, click again for descending. The default sort is created_at descending.
| Column | Sort key | Notes |
|---|---|---|
| Key | key | Full license string. Click to copy. |
| App | app_name | App name, falls back to a truncated app_id if the app is gone. |
| User | used_by | Username bound to the key, or unactivated. |
| HWID | hwid | Hardware ID captured at activation. Click to copy. |
| IP | ip | Last known IP. Click to copy. |
| Duration | duration_sort | Human label: 30 days, 1 week, lifetime. |
| Activated | activated_at | Relative time. Derived from expires_at minus duration_value * unit. |
| Created | created_at | Relative time the key was minted. |
| Last Login | last_login | Relative time of the most recent successful client login. |
| Status | status_calc | Colored badge: active, unactivated, banned, expired. |
Hovering a truncated timestamp cell reveals the full local date and time as a tooltip.
Row actions
Each row exposes up to four icon buttons on the right, gated by role.
| Icon | Action | Endpoint | Gate |
|---|---|---|---|
| Clock | Extend / reduce | POST /licenses/{id}/extend | Owner and Admin only. Disabled for lifetime and for keys that have not been activated. |
| Refresh | Reset HWID | POST /licenses/{id}/reset-hwid | reset_hwid permission. Disabled when no HWID is bound. |
| Prohibit / Check | Ban / Unban | PATCH /licenses/{id}/ban | ban_unban_keys. Toggles based on current status. |
| Trash | Delete | DELETE /licenses/{id} | delete_keys. Permanent. |
Reseller accounts see only the actions their permission set allows. See Team and Resellers for the full permission matrix.
Extend
The extend dialog shows the key text, its current expiry, and a Days field. Positive values add time, negative values subtract time, and zero is rejected client-side.
The request body is:
{
"value": 7,
"unit": "days",
"subtract": false
}
Negative input is sent as value: abs(days) with subtract: true.
Reset HWID
A confirm-only modal. On confirm the HWID binding is cleared, and the next successful client login binds a fresh one.
Use this for legitimate hardware swaps and support tickets. It is not a workaround for HWID lock issues that have a real root cause; see HWID Lock if you keep hitting mismatches.
Delete
Permanent removal. If the key is currently bound to a user, the dialog surfaces that fact so you know whose access you are about to revoke. Deletion takes effect immediately for the next client request from that user.
Bulk actions
Selecting rows via the leftmost checkbox column opens an action bar above the table. The header checkbox toggles every row currently visible under the active filters.
The bar shows the selection count plus buttons for extend, unban, ban, delete, and clear. Availability follows the same permission gates as the row actions.
Extend uses the number input next to the button as the day delta and accepts negatives to subtract. The frontend sends one request per selected row, so lifetime keys or unactivated keys in a mixed selection are simply counted in the failed tally that the closing toast reports as X succeeded, Y failed.
Bulk delete is not reversible. There is no soft-delete or trash bin, and active users on any deleted key lose access on their next request.
Generate Keys
Generate lives on its own route in the sidebar. It requires the generate_keys permission, and resellers without it get an access-denied card instead of the form. If you have no applications yet, the panel points you to Applications to create one first.
Form
The generator collects these fields before minting.
| Field | Range | Notes |
|---|---|---|
| Application | required | Your chosen app_id is echoed under the field for reference. |
| Quantity | 1 to 500 | Keys minted in this batch. |
| Level | integer, min 1 | Access-tier tag written onto every key. Your app decides what it means. |
| Duration chips | 30d / 90d / 180d / 365d / lifetime | Presets. lifetime disables the day input. |
| Duration input | 1 to 3650 | Explicit day count when the chips do not fit. |
| Key format | mask string | See below. |
| Character classes | Uppercase / Lowercase / Numbers | At least one must be checked. |
| Notes | optional | Free text stored on every key in the batch. |
Masks
The mask is the literal template the generator emits. Any *, #, or X character becomes a random slot filled from the enabled character set. Everything else passes through unchanged, including letters, digits, dashes, underscores, and spaces.
| Mask | Sample output (uppercase + numbers) |
|---|---|
****-****-****-**** | A3F9-Z1QK-7BME-XV20 |
OBS-********** | OBS-K7Q2M9AZ0P |
promo-####-#### | promo-8XA2-Q19Z |
A live preview below the mask input rerenders as you change the mask or the character classes.
Credits and pricing
Each application defines a key_costs map. The generator looks up the price per key from the closest matching tier and multiplies by the quantity.
per_key = key_costs["days:{N}"] when the exact tier exists
= key_costs["lifetime"] for lifetime keys
= max(1, ceil((days / 30) * key_costs["days:30"])) fallback when days:30 is set and greater than zero
= 1 when no key_costs is configured, or the fallback tier is missing or zero
Hours are rounded up to at least one day, weeks are multiplied by seven, and months are multiplied by thirty before the tier lookup.
The footer of the form shows total (per-key x quantity). For reseller accounts it also shows your current balance and blocks generation when the total exceeds it.
Request
The submit button posts:
POST /apps/{app_id}/licenses
Content-Type: application/json
{
"amount": 25,
"duration": 30,
"unit": "days",
"level": 1,
"mask": "****-****-****-****",
"alphabet": "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
"note": "launch batch"
}
The response is an array of created license objects. The panel extracts every key field and renders them in a scrollable result panel.
Result panel
The result panel appears once, right after generation. It gives you:
- A
Copy allbutton that puts every key on the clipboard, newline-separated. - A
Download .txtbutton that emits a header block followed by the keys as a plain text file. The filename combines the app name, the key count, and a timestamp. - A per-row copy button for individual keys.
- An
Open applink back to the application detail page.
A yellow banner sits above the list: these keys are shown once. save them now.
The plaintext key list is not retrievable from the API after the response has closed. Only the metadata rows in the Manage Keys table persist. If you close the panel without copying or downloading, those keys are gone from your side.
Recent batches
The last five batches from this browser session are cached in sessionStorage under generateKeys.recentBatches. Click a chip to re-open the same key list in the result panel without hitting the API. A clear button next to the chips wipes the cache.
Recent batches do not survive a browser restart, and they are not shared across devices or tabs.