License and Licensed Brand Administration¶
This guide covers the license service's license records and the brands registered under those licenses. It does not describe the separate SaaS brand management system. The console's Licenses section uses the license service routes below.
License administration routes require the existing superadmin permission.
GET /v1/license/license/check is the public status check. License creation
supports two document versions: the existing route continues to issue version 1
by default, and a version-aware route can explicitly issue version 1 or 2.
Existing records are not reissued or changed by ordinary license updates. An
eligible existing v1 license can move to protocol v2 automatically when a
current Main Server connects to an upgraded license service; this keeps the
same license record, UID, key pair, term, settings, brand allowance, and limits.
No Console action or replacement license is required. See
Automatic v1-to-v2 migration
for rollout, machine-binding, and recovery behavior.
License status values already used by the service are:
| Value | Name | Description |
|---|---|---|
0 |
Trial | Trial license. |
1 |
Active | Active license. |
2 |
Development | Development license. |
3 |
Expired | Expired license. |
4 |
Blocked | Blocked license. |
License type values already used by the service are:
| Value | Name | Description |
|---|---|---|
0 |
Standard (STANDART) |
Standard license type; STANDART is the existing service name. |
1 |
Multibrand | Multibrand license type. |
2 |
Partner | Partner license type. |
These tables describe the existing numeric values. The create/update validators
accept type and license status as numbers and do not add enum-range checks.
Brand status values¶
The status value on a licensed brand is an administrative state:
| Value | Name | Meaning |
|---|---|---|
0 |
Active | The brand is active. |
1 |
Terminated | The brand is terminated. |
2 |
Stopped | The brand is stopped. |
3 |
Deleted | The brand is marked deleted and retained in the registry; the record is not physically removed. |
4 |
Pending | The brand is pending. |
An active brand also needs local enable = 1 and a usable license. The registry
status is an administrative permission; it is not a connection-health indicator.
enable is a separate value reported by the running Main Server. Synchronization
can update enable and terminal_host; it preserves the administrative status,
record ID, and brands omitted from a later report. Newly discovered brands use the
database's existing default status. The service refreshes the returned brand list
after synchronization.
The optional license field brands stores the configured brand allowance.
Omitting it when creating or updating a license preserves the existing/default
value. Legacy clients that do not negotiate brand policy keep their existing
behavior. For a protocol-v2 client that opts into brand policy version 1, the
allowance controls new API admission and the Main Server's local creation guard;
see Brand policy negotiation and admission.
Routes¶
The license service routes have a license namespace in the console gateway.
The first path column is the service-local route from its contract; the second
is the console gateway path:
| Method | Service-local route | Console gateway path | Permission | Purpose |
| --- | --- | --- | --- |
| GET | license/list | /v1/license/license/list | superadmin | List/filter licenses and request aggregate values. |
| GET | license/id | /v1/license/license/id | superadmin | Read one license by ID. |
| GET | license.public_key | /v1/license/license/public-key | Public | Return a license's trading-server Ed25519 public key and current active state for Data Lake verification. |
| POST | license | /v1/license/license | superadmin | Create a version 1 license; optional protocol_version must be 1. |
| POST | license/issue | /v1/license/license/issue | superadmin | Create a version 1 or 2 license with an explicit protocol version. |
| GET | license/capabilities | /v1/license/license/capabilities | superadmin | Return supported issuance protocol versions. |
| GET | license/brands/policy | /v1/license/license/brands/policy | superadmin | Read effective brand admission and terminal approval policy for a license. |
| PUT | license/brands/terminal | /v1/license/license/brands/terminal | superadmin | Approve or revoke a registered brand's terminal host. |
| PUT | license | /v1/license/license | superadmin | Update a license. |
| GET | brands/list | /v1/license/brands/list | superadmin | List/filter brands registered under licenses. |
| GET | brand/id | /v1/license/brand/id | superadmin | Read one registered brand by ID. |
| PUT | brand | /v1/license/brand | superadmin | Update a registered brand's administrative values. |
| GET | license/check | /v1/license/license/check | Public | Return the license's current validity string. |
Get a license public key — GET /v1/license/license/public-key¶
This public route is intended for verifiers that need to resolve a Data Lake
token's lic_uuid. It does not require authentication. The request must use
HTTPS to the trusted license-service host configured by the verifier; do not
accept a license-service URL from a JWT header, claim, or caller-supplied field.
| Query parameter | Type | Required | Validation |
|---|---|---|---|
lic_uuid |
string | Yes | 1–128 ASCII characters from A-Z, a-z, 0-9, _, or -. |
The response is a JSON object with exactly these fields:
| Field | Type | Description |
|---|---|---|
lic_uuid |
string | The requested license UID. |
kid |
string | Lowercase SHA-256 hex of the raw 32-byte Ed25519 public key. This is the Data Lake JWT kid; it is not the runtime API's DER/SPKI key_fingerprint. |
alg |
string | EdDSA. |
public_key |
string | PEM-encoded Ed25519 public key belonging to the trading-server license identity. The private key and encrypted license capsule are never returned. |
status |
integer | Current license status code: 0 trial, 1 active, 2 development, 3 expired, or 4 blocked. |
active |
boolean | true only for status 0, 1, or 2 while the license has not expired, according to the license-service database clock. |
expires_at |
integer | License expiry as Unix seconds. This is license metadata, not a token-expiration claim. |
Example response (the Ed25519 key below is a synthetic test fixture; its
verification timestamp is 1800000000 Unix seconds):
{
"lic_uuid": "fixture-public-key-license",
"kid": "6a3803d5f059902a1c6dafbc9ba4729212f7caac08634cc3ae76b27529f03827",
"alg": "EdDSA",
"public_key": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAgTl3Dqh9F19Wo1Rmw0x+zMuNipG07jeiXfYPW4/Js5Q=\n-----END PUBLIC KEY-----\n",
"status": 1,
"active": true,
"expires_at": 1800003600
}
The route can return a public key for an inactive license so historical tokens
can still be identified or cryptographically checked. A verifier must reject
new authorization when active is false; the response does not specify a
cache duration. Check active for each new authorization, or use an explicitly
bounded cache policy that accounts for license-status changes.
The endpoint does not invalidate already-issued tokens or define session
revocation.
| HTTP status | Error type | Meaning |
|---|---|---|
404 |
LICENSE_NOT_FOUND |
No license exists for lic_uuid. |
422 |
INVALID_LICENSE_UID or VALIDATION_ERROR |
The handler rejects a missing or malformed UID as INVALID_LICENSE_UID; framework parameter validation may reject it first as VALIDATION_ERROR. |
503 |
LICENSE_PUBLIC_KEY_UNAVAILABLE |
The stored key is not a valid Ed25519 public key or the license metadata cannot be safely evaluated. The response does not reveal which validation failed. |
License routes¶
List licenses — GET /v1/license/license/list¶
All query fields are optional. The response is an object containing rows when
select is supplied, plus one array for each requested aggregate.
| Field | Type | Description |
|---|---|---|
limit |
number | Page size, 1–100; defaults to 20. |
offset |
number | Row offset; defaults to 0. |
orderBy |
array | [field, direction], for example ["id", "DESC"]. |
groupBy |
array | Grouping field. |
select |
array | Select fields; ["*"] selects all fields available to the list route. |
where |
array | AND filters in [field, operator, value] form. |
orWhere |
array | OR filters in [field, operator, value] form. |
whereNot |
array | Negative filters. |
orWhereNot |
array | OR negative filters. |
whereIn |
array | Membership filters in [field, values] form. |
whereNotIn |
array | Non-membership filters in [field, values] form. |
whereBetween |
array | Range filters in [field, [from, to]] form. |
orWhereBetween |
array | OR range filters. |
whereNotBetween |
array | Negative range filters. |
orWhereNotBetween |
array | OR negative range filters. |
include_server |
boolean | Optional. Defaults to false. When true, add a compact server presence summary to each license row. |
count |
array | Fields to count. |
sum |
array | Fields to sum. |
min |
array | Fields for which to return the minimum. |
max |
array | Fields for which to return the maximum. |
avg |
array | Fields to average. |
The license list response fields are:
| Field | Type | Description |
|---|---|---|
id |
number | License record ID. |
uid |
string | License UID used by the license check and Main Server. |
private_key |
string | Existing license private-key field. Treat as sensitive. |
public_key |
string | Existing public-key field. |
brand |
string | License brand/name label. |
initDate |
date/time | License initialization timestamp. |
expireDate |
date/time | License expiration timestamp. |
status |
number | License status code; see the table above. |
type |
number | License type code; see the table above. |
settings |
JSON value / string | Parsed stored JSON, or the original legacy value if parsing fails. |
brands |
integer/number | Stored configured brand allowance. |
comment |
string | Operator comment. |
lic |
string | Existing license document value. It may contain encoded/encrypted license data. |
The query accepts the fields listed above. settings is parsed as JSON when
possible. The list response includes rows only when select is supplied;
aggregate arrays are included when requested. By default, the response retains
the existing shape and does not add server-presence fields. The optional
include_server=true flag adds a server object to each selected row; it does
not include the full runtime telemetry object. Presence is a virtual summary,
not a license-table column, so it cannot be selected, filtered, grouped, or
sorted as a SQL field. Presence is loaded with one bounded, non-locking SQL
LEFT JOIN query for the unique UIDs on the current page (up to 100); the
service does not make a separate query per row.
The selected row must contain uid for its presence to be matched; the service
does not secretly add the UID to a projection. Rows without a usable UID and
grouped results receive an unknown summary.
Example request:
GET /v1/license/license/list?select[]=*&count[]=id&limit=20&offset=0
Example response:
{
"rows": [
{
"id": 17,
"uid": "example-license-uid",
"private_key": "[fictitious private key placeholder]",
"public_key": "[fictitious public key placeholder]",
"brand": "example-broker",
"initDate": "2026-01-01T00:00:00.000Z",
"expireDate": "2027-01-01T00:00:00.000Z",
"status": 1,
"type": 1,
"settings": {},
"brands": 5,
"comment": "Example license",
"lic": "[fictitious encoded license document placeholder]"
}
],
"count": [
{ "id": 1 }
]
}
Opt in to server presence by adding include_server=true. For example, this
request selects four license fields and includes the server summary:
GET /v1/license/license/list?select[]=id&select[]=uid&select[]=brand&select[]=status&include_server=true&limit=20&offset=0
The complete response for those selected fields is:
{
"rows": [
{
"id": 17,
"uid": "example-license-uid",
"brand": "example-broker",
"status": 1,
"server": {
"status": "online",
"last_seen_at": 1791300000,
"offline_after_seconds": 90,
"received_at": 1791300000,
"ips": ["192.0.2.10", "2001:db8::10"]
}
}
]
}
The nested server summary contains:
| Field | Type | Description |
|---|---|---|
status |
string | online, offline, or unknown. |
last_seen_at |
integer or null | Database Unix timestamp of the most recent authenticated owner activity. |
offline_after_seconds |
integer | Staleness threshold; 90. |
received_at |
integer or null | Database Unix timestamp when the latest valid telemetry sample was accepted. |
ips |
string array | Unique valid IPv4/IPv6 interface addresses from the normalized telemetry snapshot. These are local interface addresses and are not guaranteed to be public NAT addresses. |
online requires fresh committed acquire/renew owner activity within the
90-second threshold and a matching current authority owner (run_id,
session_id, epoch, and seq). A persisted owner release or activity at least
90 seconds old is offline. Missing authority/presence, unavailable schema,
query errors, or an owner-sequence catch-up state is unknown. For the same
owner identity, previously accepted IPs remain visible when the server becomes
offline or presence is temporarily unknown while sequence state catches up.
IPs are hidden when the stored presence belongs to a different authority owner
identity (run_id, session_id, or epoch differs) or no authority record
exists; a sequence-only catch-up mismatch does not hide IPs for the same owner.
Older license-service backends may omit server even when
include_server=true; treat a missing object as unknown with no reported
IPs. A successful response with empty IPs clears the previous IP display;
only a failed refresh retains the last-known display as stale data. Full telemetry remains
available from the runtime details route; see
License Runtime Protocol v2 for the full telemetry schema
and runtime-presence details.
In the console, the Licenses table displays the server and ips summary
columns and refreshes the current paginated query every 30 seconds; it does not
send a separate presence request per row. The console uses a 90-second
monotonic staleness deadline. If a refresh fails, it keeps the last-known IPs
as stale display data rather than treating them as a fresh server report.
Read a license — GET /v1/license/license/id¶
Query field:
| Field | Type | Description |
|---|---|---|
id |
number | Optional in the schema; minimum 1, maximum 10000000, default 20. Provide the intended ID explicitly. |
On success, the route returns the stored license row as an object. The known row
fields are id, uid, private_key, public_key, brand, initDate,
expireDate, status, type, settings, brands, comment, and lic.
settings is parsed as JSON when possible. Key fields and the license document
are represented by fictitious placeholders in this example; never copy these
placeholders into a live record.
{
"id": 17,
"uid": "example-license-uid",
"private_key": "[redacted example private key]",
"public_key": "[redacted example public key]",
"brand": "example-broker",
"initDate": "2026-01-01T00:00:00.000Z",
"expireDate": "2027-01-01T00:00:00.000Z",
"status": 1,
"type": 1,
"settings": {},
"brands": 5,
"comment": "Example license",
"lic": "[redacted example license document]"
}
If the record does not exist, the route returns a Moleculer error with numeric
code 404 and type LICENSE_NOT_FOUND.
Create a version 1 license — POST /v1/license/license¶
This is the backward-compatible creation route. Requests that omit
protocol_version continue to issue version 1. For explicit version selection,
use the version-aware route below.
| Field | Type | Required | Description |
|---|---|---|---|
type |
number | Yes | License type value accepted by the existing service. |
status |
number | Yes | License status value accepted by the existing service. |
expireDate |
date/string | No | Expiration date; date conversion is enabled. |
brand |
string | Yes | License brand/name label. |
brands |
integer | No | Stored brand allowance, from 0 through 2147483647. |
settings |
object | No | License settings. |
comment |
string | No | Operator comment. |
protocol_version |
integer | No | If supplied, must be 1. Omitted values default to version 1. |
The route returns a one-element array containing the new record ID:
[17]
Example request (the legacy request shape remains valid):
{
"type": 1,
"status": 1,
"expireDate": "2027-01-01 00:00:00",
"brand": "example-broker",
"brands": 5,
"settings": {},
"comment": "Example license"
}
Create a versioned license — POST /v1/license/license/issue¶
This route requires the caller to select a supported protocol version explicitly.
It accepts the same license fields as the legacy create route, with required
protocol_version:
| Field | Type | Required | Description |
|---|---|---|---|
type |
number | Yes | License type value accepted by the existing service. |
status |
number | Yes | License status value accepted by the existing service. |
expireDate |
date/string | No | Expiration date; date conversion is enabled. |
brand |
string | Yes | License brand/name label. |
brands |
integer | No | Stored brand allowance, from 0 through 2147483647. |
settings |
object | No | License settings. |
comment |
string | No | Operator comment. |
protocol_version |
integer | Yes | Supported values are 1 and 2. Other values are rejected. |
The response is the same one-element ID array as the legacy create route:
[18]
Example request:
{
"type": 1,
"status": 1,
"expireDate": "2027-01-01 00:00:00",
"brand": "example-broker-v2",
"brands": 5,
"settings": {},
"comment": "Prepared for a version 2 runtime",
"protocol_version": 2
}
When protocol_version is 2, the service checks runtime-v2 schema readiness
before generating license keys. If unavailable, creation fails with HTTP 503
and type LICENSE_V2_NOT_READY; it does not retry as version 1. If the
transaction that inserts the license and its authority record fails, the
request returns HTTP 500 with type LICENSE_V2_ISSUE_FAILED and does not
return a license ID.
An unexpected failure in a runtime-authority administration route returns
HTTP 500 with type LICENSE_V2_ADMIN_FAILED.
Default module settings on license creation¶
Both license creation routes fill missing settings.modules values with these
defaults:
| Setting | Type | Default |
|---|---|---|
backup |
boolean | true |
plugin |
boolean | true |
copy_trade |
boolean | false |
prop_trade |
boolean | false |
The defaults apply if settings or settings.modules is omitted, and fill only
missing keys. Explicit values—including false—are kept. Other module keys and
unrelated settings are retained. For example, creating a license with:
{
"settings": {
"region": "eu",
"modules": {
"backup": false,
"external_feature": "enabled"
}
}
}
stores the resulting settings as:
{
"region": "eu",
"modules": {
"backup": false,
"plugin": true,
"copy_trade": false,
"prop_trade": false,
"external_feature": "enabled"
}
}
The Licenses Add/Edit form exposes these four module settings as checkboxes for the licensed-license record. The console preserves unknown module keys and unrelated settings when saving. These defaults do not migrate or rewrite existing license records.
Read issuance capabilities — GET /v1/license/license/capabilities¶
This route takes no query parameters or request body. It returns the protocol
versions currently available for issuance and the default used by legacy
creation. Before runtime-v2 schema readiness, protocol_versions is [1];
when the runtime authority schema is ready, it is [1, 2]:
{
"protocol_versions": [1, 2],
"default_protocol_version": 1
}
When runtime v2 is not ready, the response is:
{
"protocol_versions": [1],
"default_protocol_version": 1
}
The response fields are:
| Field | Type | Description |
|---|---|---|
protocol_versions |
integer array | Protocol versions supported for issuance by this service. |
default_protocol_version |
integer | Version used when the legacy create route omits protocol_version; currently 1. |
Clients can use this response to offer version 2 only when the service confirms
support. If the route is unavailable or its response is unrecognized, retain the
version 1 creation path. If a version 2 request reaches a backend that does not
support it, the request must remain an error; clients must not retry it through
the legacy route or silently downgrade it to version 1. This also applies when
previous capability information has become stale. V2 issuance fails with
503 LICENSE_V2_NOT_READY before key generation when runtime schema is not ready.
For either create route, an unsupported or invalid protocol_version is
rejected. The version-aware route requires the field; the legacy route accepts
only 1 when the field is present. Requests that fail the framework's ordinary
schema validation (for example, a missing required field or a non-integer value)
receive its standard validation error. A handler-level protocol-version
rejection, including a null or unsupported value that reaches the handler, has
numeric error code 422 and type INVALID_LICENSE_PROTOCOL_VERSION.
Encrypted license capsule payload¶
The create routes return an ID, not the decrypted capsule. The capsule remains inside the existing encrypted license document. The following schematic JSON shows its payload shape after decryption; all values are fictitious placeholders, not usable credentials or key material.
Version 1 payload (there is no protocol_version field):
{
"uid": "[fictitious license UID]",
"login": "[fictitious login placeholder]",
"pwd": "[fictitious password placeholder]",
"private_key": "[fictitious private key placeholder]",
"public_key": "[fictitious public key placeholder]"
}
Version 2 payload (the only added field is the integer protocol_version):
{
"uid": "[fictitious license UID]",
"login": "[fictitious login placeholder]",
"pwd": "[fictitious password placeholder]",
"private_key": "[fictitious private key placeholder]",
"public_key": "[fictitious public key placeholder]",
"protocol_version": 2
}
| Field | Type | Version 1 | Version 2 |
|---|---|---|---|
uid |
string | Present | Present |
login |
string | Present | Present |
pwd |
string | Present | Present |
private_key |
string | Present | Present |
public_key |
string | Present | Present |
protocol_version |
integer | Absent | Present with value 2 |
Main Server runtime version handling¶
The current Main Server runtime treats a missing protocol_version as version 1.
Integer 1 uses the established runtime and wire protocol. Integer 2 selects
the authenticated v2 NATS exchange and requires an active server-issued lease.
Duplicate protocol_version members; null, boolean, string, array, or object
values; floating-point values including 1.0 and 2.0; and integers other than
1 or 2 are rejected during capsule loading. The runtime never falls back to
v1 after a v2 selection or exchange failure.
| Capsule value | Runtime behavior |
|---|---|
| Field absent | Interpreted as v1. |
Integer 1 |
Uses the existing license exchange. |
Integer 2 |
Uses v2 TLS, authenticated session setup, and renewable runtime lease. |
| Duplicate/invalid/unknown value | Capsule loading fails; no protocol downgrade occurs. |
The public ILicenseApi SDK interface remains at ABI version 4. V2 uses the
optional ILicenseLeaseApi side interface for host-enforced lease activation and
invalidation; it adds no fields or virtual functions to the existing ABI.
Runtime telemetry is a separate optional feature exposed through
ILicenseTelemetryApi; it does not change the license capsule, is not required
for v2 leases, and does not change v1. When present, a v2 runtime includes
telemetry in its encrypted acquire/renew reports, and the runtime administration
GET can return the latest snapshot and online/offline status. See
License Runtime Protocol v2 for the complete telemetry
schema and response fields.
See License Runtime Protocol v2 for cryptographic wire fields, lease lifecycle, operator binding/enrollment, TLS setup, and runtime access behavior.
Update a license — PUT /v1/license/license¶
| Field | Type | Required | Description |
|---|---|---|---|
id |
number | Yes | License record ID. |
type |
number | Yes | License type value accepted by the existing service. |
status |
number | Yes | License status value accepted by the existing service. |
expireDate |
date/string | No | Expiration date; date conversion is enabled. |
brand |
string | Yes | License brand/name label. |
brands |
integer | No | Stored brand allowance, from 0 through 2147483647; omitted values remain unchanged. |
settings |
object | No | License settings. |
comment |
string | No | Operator comment. |
The response is the numeric number of affected rows, including 0 when no row
matched:
PUT does not apply creation defaults. If settings is omitted, the stored
settings remain unchanged. If supplied, the update does not fill missing module
keys with the creation defaults.
1
Example request:
{
"id": 17,
"type": 1,
"status": 1,
"expireDate": "2027-01-01 00:00:00",
"brand": "example-broker",
"brands": 5,
"settings": {},
"comment": "Example license"
}
Licensed brand routes¶
List registered brands — GET /v1/license/brands/list¶
The query fields and pagination/aggregate behavior match the license list route.
The recognized licensed-brand fields are id, lic_id, terminal_host,
brand, status, enable, settings, time_create, and time_update.
settings is parsed as JSON when possible.
The registered-brand response fields are:
| Field | Type | Description |
|---|---|---|
id |
number | Licensed-brand record ID. |
lic_id |
number | ID of the owning license record. |
terminal_host |
string | Host reported by the running Main Server. |
brand |
string | Registered terminal brand identifier. |
status |
number | Administrative status, 0 through 4 as listed above. |
enable |
number | Last Main Server-reported enabled value: 0 disabled, 1 enabled. Legacy update requests may also supply it. |
settings |
JSON value / string | Parsed stored JSON, or the original legacy value if parsing fails. |
time_create |
date/time | Record creation timestamp. |
time_update |
date/time | Last update timestamp. |
Example request:
GET /v1/license/brands/list?select[]=*&count[]=id&limit=20&offset=0
Example response:
{
"rows": [
{
"id": 42,
"lic_id": 17,
"terminal_host": "trade.example.test",
"brand": "example-terminal-brand",
"status": 0,
"enable": 1,
"settings": {},
"time_create": "2026-01-01T00:00:00.000Z",
"time_update": "2026-01-02T00:00:00.000Z"
}
],
"count": [
{ "id": 1 }
]
}
Read a registered brand — GET /v1/license/brand/id¶
Query field:
| Field | Type | Description |
|---|---|---|
id |
number | Optional in the schema; minimum 1, maximum 10000000, default 20. Provide the intended ID explicitly. |
The successful response is the stored brand row object, with settings parsed as
JSON when possible:
{
"id": 42,
"lic_id": 17,
"terminal_host": "trade.example.test",
"brand": "example-terminal-brand",
"status": 0,
"enable": 1,
"settings": {},
"time_create": "2026-01-01T00:00:00.000Z",
"time_update": "2026-01-02T00:00:00.000Z"
}
If the record does not exist, the route returns a Moleculer error with numeric
code 404 and type BRAND_NOT_FOUND.
Update a registered brand — PUT /v1/license/brand¶
| Field | Type | Required | Description |
|---|---|---|---|
id |
number | Yes | Licensed-brand record ID. |
status |
number | Yes | Administrative status, 0 through 4 as listed above. |
enable |
number | No | Main Server-reported enabled value. Existing clients may continue sending it. |
brand |
string | No | Existing registered brand name. |
time_update |
date/string | No | Update timestamp; date conversion is enabled. If omitted, the service uses its current time. |
settings |
object | No | Brand settings. |
The legacy console continues sending id, enable, brand, and status.
Newer clients can send only id and status to change the administrative
state. The response is the numeric affected-row count:
1
For a policy-enrolled brand, the registry brand value is immutable through
this route. Sending its current name is accepted; attempting to rename it
returns numeric error code 409 with type BRAND_POLICY_RENAME_FORBIDDEN.
The supplied enable value is ignored for an enrolled record because local
enablement is reported by the Main Server and remains server-authoritative.
Unenrolled legacy records retain the prior update behavior. These enrolled-row
updates share the license-row lock used by renewal, reconciliation, and terminal
approval, so those operations serialize against one another.
The existing numeric validators do not newly restrict status or enable to
these documented enum ranges. Use the defined values for runtime compatibility.
Example request:
{
"id": 42,
"status": 2,
"enable": 1,
"brand": "example-terminal-brand"
}
Status changes trigger a best-effort refresh request using the existing license event channel. The saved database update does not depend on a running Main Server or on delivery/application acknowledgement. Main Server synchronization can report local enablement and host information, while administrative status remains managed by the license service.
Read brand policy — GET /v1/license/license/brands/policy¶
Query parameter:
| Field | Type | Required | Description |
|---|---|---|---|
id |
integer | Yes | License record ID. |
The route requires superadmin and returns the effective policy and per-brand
admission view. available indicates whether policy operations are available;
enrolled indicates whether this license has entered sticky policy enforcement.
The first successful policy-v1 negotiation records existing brands as the
baseline, so an initially over-limit license does not lose those brands solely
because of enrollment. Other status and local-enable restrictions continue to
apply.
| Field | Type | Description |
|---|---|---|
license_id |
integer | License record ID. |
policy_version |
integer | Brand policy contract version; currently 1. |
available |
boolean | Whether the policy is available for this license. |
enrolled |
boolean | Whether the license has entered policy enforcement. |
allowance |
nonnegative integer | Configured brand allowance. For policy version 1, 0 means unlimited. |
unlimited |
boolean | Whether the policy has no finite admission limit. Use this effective flag when presenting the limit. |
used |
nonnegative integer | Number of present brands consuming allowance under this policy. Previously admitted present brands can keep working after a limit decrease, so this value can exceed a positive allowance. Locally disabled brands still consume allowance; deleted and historical rows that are no longer reported do not. |
current_reported |
nonnegative integer | Number of brands in the current Main Server report. |
brands |
array | Per-registry-row admission and terminal approval information. |
Each item in brands contains:
| Field | Type | Description |
|---|---|---|
registry_id |
positive integer | Licensed-brand registry record ID. |
brand |
string | Registered Main Server brand identifier. |
status |
integer | Administrative brand status: 0 active, 1 terminated, 2 stopped, 3 deleted, 4 pending. |
enable |
integer | Main Server's local enable value (0 disabled, 1 enabled); local enablement remains enforced by the Main Server. |
present |
boolean | Whether the brand appears in the current Main Server report. |
reported_host |
string or null |
Host most recently reported by the Main Server, if present. |
approved_host |
string or null |
Canonical host approved for terminal access, if any. |
terminal_approval |
string | approved, pending, revoked, or invalid. |
api_allowed |
boolean | Whether this present, locally enabled brand is admitted for API consumers under the policy and its administrative status. |
terminal_allowed |
boolean | Whether this policy permits the brand's terminal host; Main Server status and local enable checks still apply. |
admission_reason |
string | legacy, admitted, limit_reached, not_reported, deleted, terminated, stopped, or disabled. |
Example request:
GET /v1/license/license/brands/policy?id=17
Example response:
{
"license_id": 17,
"policy_version": 1,
"available": true,
"enrolled": true,
"allowance": 2,
"unlimited": false,
"used": 1,
"current_reported": 1,
"brands": [
{
"registry_id": 42,
"brand": "example-terminal-brand",
"status": 0,
"enable": 1,
"present": true,
"reported_host": "trade.example",
"approved_host": "trade.example",
"terminal_approval": "approved",
"api_allowed": true,
"terminal_allowed": true,
"admission_reason": "admitted"
}
]
}
The route returns numeric error code 404 with type LICENSE_NOT_FOUND when the
license does not exist, or 503 with type LICENSE_V2_NOT_READY when the
runtime policy schema is not ready. An unexpected untyped administration failure
returns numeric error code 500 with type LICENSE_V2_ADMIN_FAILED.
Approve or revoke a terminal host — PUT /v1/license/license/brands/terminal¶
This superadmin route returns the same complete policy object as the GET route.
| Field | Type | Required | Description |
|---|---|---|---|
id |
positive integer | Yes | Licensed-brand registry record ID. |
approved |
boolean | Yes | true approves the current reported host; false revokes terminal permission. |
reported_host |
string, max 2048 characters | Yes | Host currently reported by the Main Server. An empty string is accepted only when approved is false. |
Hosts are canonicalized before comparison: valid ports are removed, DNS names are
lowercased, and a trailing DNS dot is removed. IP addresses are normalized and
IPv6 is compressed. A port must contain at most five digits and be in the range
1–65535. Schemes, paths, user information, whitespace, and invalid ports
are rejected. A host change after approval requires an explicit new approval.
Approving a pending (status = 4) brand atomically changes its
administrative status to active (0). Stopped, terminated, and deleted brands
are never silently reactivated by this operation. Revocation removes terminal
permission without changing API admission.
Example request:
{
"id": 42,
"approved": true,
"reported_host": "trade.example"
}
To revoke terminal approval, send the required reported_host as an empty
string:
{
"id": 42,
"approved": false,
"reported_host": ""
}
The response is the same DTO shown in the GET example. Errors are returned as Moleculer errors with these numeric codes and types:
| Numeric code | Type | Meaning |
|---|---|---|
404 |
LICENSE_NOT_FOUND |
The requested license does not exist. |
404 |
BRAND_NOT_FOUND |
The licensed-brand registry record does not exist. |
409 |
BRAND_POLICY_NOT_ENROLLED |
The license has not entered policy-v1 enforcement. |
409 |
TERMINAL_HOST_IN_USE |
The canonical host is already approved for another brand under any license. |
409 |
TERMINAL_HOST_CHANGED |
The supplied host no longer matches the current canonical reported host. |
422 |
INVALID_TERMINAL_HOST |
Approval was requested for an empty or unsupported host. |
500 |
LICENSE_V2_ADMIN_FAILED |
An unexpected untyped administration failure. |
503 |
LICENSE_V2_NOT_READY |
The runtime policy schema is not ready. |
503 |
BRAND_POLICY_NOT_READY |
The brand policy is not ready for this operation. |
Brand policy behavior¶
Brand API admission and terminal host approval are separate decisions. API
admission does not itself require terminal-host approval. Under
policy version 1, present, locally enabled active (0) and pending (4)
brands may be admitted for API use within the configured allowance. A locally
disabled brand has api_allowed: false with admission_reason: "disabled", but
still uses allowance. Terminated (1), stopped (2), and deleted (3) brands
remain blocked. A historical registry row absent from the current report does
not use allowance. When a positive finite allowance is full, additional brand
admission is denied. Already admitted and still-present brands continue to work
after the allowance is lowered, even when used exceeds the new limit; lowering
the limit does not create an outage. The baseline at first enrollment also
preserves brands already present before the first policy-v1 report. A previously
admitted non-baseline brand that disappears from a later report loses its
admission; if it returns, it must pass the current quota again. Deleted brands
do not consume allowance.
For terminal use, a valid reported host must be approved. On first enrollment,
an already active brand can be bootstrapped once to the valid host captured in
the pre-synchronization baseline. If the initial report already contains a host
change, that new host requires explicit operator approval. Subsequent host
changes also require operator approval. A pending brand
becomes active only when its terminal host is approved. The Main Server still
enforces the local enable value and its existing administrative status checks.
Customer password recovery can resolve an explicitly supplied brand through API
admission under policy v1, even when the request has an HTTP hostname. If no
explicit brand is supplied, hostname-based resolution continues to require the
approved terminal host. Sending the recovery email still requires a configured,
non-empty terminal_host to build the reset URL; API admission does not approve
or provide that URL host.
Managing licensed brands in Console¶
The license detail page places the licensed-brand table before the server runtime details. Its summary shows admitted brands, the allowance, remaining slots, and the number of brands in the current server report. Search by brand name and use the page controls to navigate the table.
Each row shows the registry identity, server enable/report state, API admission and reason, administrative permission, reported and approved terminal hosts, and terminal access. Superadmins can change permission or approve/revoke a host from the row; terminal decisions require explicit confirmation. Copy controls preserve the full displayed host. Policy rows remain visible if the legacy registry lookup fails. Services without an enrolled policy use the legacy registry table data and show policy-dependent permissions as unknown.
License status check — GET /v1/license/license/check¶
Query field:
| Field | Type | Required | Description |
|---|---|---|---|
uid |
string | Yes | License UID to check. |
This public route returns the string "valid" only when the stored license
status equals 1; it returns "invalid" otherwise, including when no record
matches.
"valid"
Compatibility and operational notes¶
- Existing clients can continue creating licenses through
POST /v1/license/license; omittedprotocol_versiondefaults to version 1. Version-aware clients should checkGET /v1/license/license/capabilitiesand usePOST /v1/license/license/issueto request version 2. - Version 1 preserves the existing encrypted payload format. Version 2 adds the
numeric
protocol_version: 2field inside the existing AES-GCM-encrypted payload. The AES format, key generation and placement, stored license fields, and license read/update/download behavior are unchanged. No actual key material is part of this documentation. - Version is selected at creation.
PUT /v1/license/licensedoes not reissue a license or change its protocol version. Existing license records remain as issued. - The current v2 runtime uses TLS to
lic.scaletrade.comon the existing common NATS port and an authenticated lease protocol; v1 keeps the original IP endpoints and plaintext behavior. See License Runtime Protocol v2 for DNS, certificate, and rollout requirements. The capsule marker alone is not a hardware anti-clone guarantee; older Main Server binaries may ignore it and retain cached legacy permission, so migration must be controlled. - The version-aware create route requires runtime-v2 schema readiness. Pending
Knex migrations run automatically at license-service startup. If the
authority schema remains unavailable, v2 creation fails with HTTP 503
LICENSE_V2_NOT_READYbefore key generation. Existing v1 creation remains available when its legacy schema is usable. A v2 authority record is created atomically with a newly issued v2 license; older v2 capsules may need explicit operator enrollment. See License Runtime Protocol v2 for the migration ledger, retry, and partial-DDL recovery behavior. - The console's Licenses section manages the records described here. It does not manage the separate SaaS brand catalogue.
- After a successful license or brand update, the service best-effort publishes
the existing
{ "event": "lic.check" }message tolic.events.<uid>. There is no delivery or application ACK, and notification failure does not undo the saved update. For a UID protected by runtime v2, messages on this legacy subject are rejected, including brand-configuration messages; v2 clients use the authenticated lease protocol described in the linked runtime guide. - The original
lic.checkrefresh andlic.statebehavior remains for unprotected legacy licenses. The v2 runtime does not accept the legacy event path for protected UIDs. - An expiration date edit alone is not a guaranteed immediate revocation on an already-running older Main Server runtime. Confirm behavior against the deployed runtime version before relying on an expiration edit for revocation.
- The console continues sending the legacy brand update fields
id,enable,brand, andstatus, so the request passes validation on older services. The new service applies the update to the licensed-brand record. An older service retains its known brand-update table-target defect; do not assume a brand status change was saved there.
Manual compatibility stand check¶
Run these checks in a non-production stand with the credentials and license records already configured for that stand:
- Old console with new license service: list and open a license, list its
licensed brands, then save the statuses exposed by that console (the inspected
version exposes
0,2, and4). Confirm the service displays the saved status after refresh. Test1and3through the existing full-field API request if the old interface does not expose them. - New console with new license service: repeat all five status saves. Check that the console's status filters, save feedback, and viewer/read-only controls behave as intended.
- With the new console and service, let the Main Server report the brands again. Confirm each registered row keeps its ID and administrative status and that an omitted row remains registered. Change only a reported host value and confirm the host updates without resetting the administrative status.
- Make the Main Server or NATS refresh channel unavailable during an update. Confirm the license-service save remains successful, then restore connectivity and verify the next report/refresh makes the current state visible.
- Legacy license creation: create a license through
POST /v1/license/licensewithoutprotocol_versionand confirm it is issued as version 1. Repeat withprotocol_version: 1and confirm the existing version 1 document format remains unchanged. - Version-aware creation: confirm capabilities return
{ "protocol_versions": [1], "default_protocol_version": 1 }before runtime schema readiness and{ "protocol_versions": [1, 2], "default_protocol_version": 1 }after readiness. Create a version 1 license through/v1/license/license/issuewithprotocol_version: 1, then through/v1/license/licensewith the field omitted. Confirm both responses are[id]arrays and both capsules have noprotocol_versionfield. In an isolated stand, create a version 2 license and confirm the capsule contains integerprotocol_version: 2; do not use it with a deployment that must start on the current Main Server. Confirm missing and non-integer values receive schema validation errors, null and unsupported values are rejected withINVALID_LICENSE_PROTOCOL_VERSIONwhen they reach the handler, and ordinary update does not reissue an existing record. - Current Main Server protocol selection: verify a capsule with no version
and one with integer
1use the legacy path. Verify integer2connects over TLS and reaches lease acquisition; it must not fall back to v1 when TLS or lease setup fails. Verify duplicateprotocol_versionfields (including duplicate1, duplicate2, and mixed1/2),null, booleans, strings, arrays, objects, floating-point values such as2.0, and unsupported integers fail capsule loading before startup. Use stand fixtures and logs only; do not include real credentials in examples or reports. See License Runtime Protocol v2 for the complete lease and migration checks. - New console with old license service: confirm it can still create version 1 through the legacy route. Confirm version 2 stays unavailable when the old service does not report version 2 capability; the client must not silently send a version 2 request to the legacy create route.
- Brand compatibility: confirm the console sends the legacy
{ "id", "enable", "brand", "status" }request fields and that the old service accepts the request. The old service's brand-table update defect remains, and it does not recognize the added licensebrandsallowance field; verify these differences explicitly rather than treating them as supported behavior. - Brand policy v1: with a runtime-v2 license, confirm a report without
brand_policy_versionkeeps legacy admission. Then sendbrand_policy_version: 1and confirm the grant includeslicense.brand_policy_versionand the per-brand policy fields. On later renewal and reconnect, omit the marker and verify enforcement remains active. Set a finite allowance and verify first enrollment grandfathers existing rows, new admissions stop at the allowance, and locally disabled present brands still consume a slot. Verify absent historical rows do not. Set the allowance to zero and check that the API reportsunlimited: true. - Read the policy through
/v1/license/license/brands/policy, then approve and revoke terminal hosts through/v1/license/license/brands/terminal. Confirm a valid active host can be bootstrapped once, a pending brand becomes active only when approved, and stopped/terminated/deleted brands are not silently reactivated. Check canonical host comparisons, duplicate-host and changed-host conflicts, and invalid-host rejection. Confirm revocation removes terminal permission without changing API admission and that the Main Server's localenablecheck still applies. - At the Main Server, verify a positive full allowance blocks adding a new brand while already registered brands remain usable. Repeat against the deployed Main Server version; documentation of these checks does not claim that every runtime version has already passed them.
These are stand-check steps, not claims that a particular deployed runtime has passed them. Apply the protocol, migration, TLS, competing-instance, outage, and legacy-runtime checks in License Runtime Protocol v2 before enabling v2 for a production license.