Skip to content

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; omitted protocol_version defaults to version 1. Version-aware clients should check GET /v1/license/license/capabilities and use POST /v1/license/license/issue to request version 2.
  • Version 1 preserves the existing encrypted payload format. Version 2 adds the numeric protocol_version: 2 field 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/license does not reissue a license or change its protocol version. Existing license records remain as issued.
  • The current v2 runtime uses TLS to lic.scaletrade.com on 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_READY before 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 to lic.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.check refresh and lic.state behavior 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, and status, 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:

  1. 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, and 4). Confirm the service displays the saved status after refresh. Test 1 and 3 through the existing full-field API request if the old interface does not expose them.
  2. 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.
  3. 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.
  4. 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.
  5. Legacy license creation: create a license through POST /v1/license/license without protocol_version and confirm it is issued as version 1. Repeat with protocol_version: 1 and confirm the existing version 1 document format remains unchanged.
  6. 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/issue with protocol_version: 1, then through /v1/license/license with the field omitted. Confirm both responses are [id] arrays and both capsules have no protocol_version field. In an isolated stand, create a version 2 license and confirm the capsule contains integer protocol_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 with INVALID_LICENSE_PROTOCOL_VERSION when they reach the handler, and ordinary update does not reissue an existing record.
  7. Current Main Server protocol selection: verify a capsule with no version and one with integer 1 use the legacy path. Verify integer 2 connects over TLS and reaches lease acquisition; it must not fall back to v1 when TLS or lease setup fails. Verify duplicate protocol_version fields (including duplicate 1, duplicate 2, and mixed 1/2), null, booleans, strings, arrays, objects, floating-point values such as 2.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.
  8. 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.
  9. 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 license brands allowance field; verify these differences explicitly rather than treating them as supported behavior.
  10. Brand policy v1: with a runtime-v2 license, confirm a report without brand_policy_version keeps legacy admission. Then send brand_policy_version: 1 and confirm the grant includes license.brand_policy_version and 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 reports unlimited: true.
  11. 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 local enable check still applies.
  12. 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.