License Open API Document
v1.0
License Open API Document
This document is for developers who need to integrate a complete "pay → issue → activate → verify" flow into their own software (client software / promotion-only products). Each API below is described with its call timing, request parameters, response fields, and error codes.
1. Core Concepts
| Concept | Field / Term | Description |
|---|---|---|
| Software API secret | licenseApiSecret |
Automatically generated when the product enables "license activation". Visible in the product edit form in the Developer Center. Your software server uses it to compute the HMAC signature and the platform uses it to verify. Store it only on your server; never embed it in the client. |
| License code | licenseCode |
A unique code generated by the platform. Users activate your software with it. |
| Machine code | machineCode |
A device identifier generated by the client software (unique per computer; hash a stable machine fingerprint, at least 8 characters). |
| Activation token | activationToken |
Issued by the platform during activation / code generation. The software stores it and sends it back on every startup verification. |
| Client order ID | clientOrderId |
The order ID of the in-software payment (generated by the client or your server). The platform deduplicates by "product + order ID" to prevent duplicate charges or duplicate code issuance. |
2. Preparation
- Enable "license activation" when publishing the product and save the generated Software API secret (licenseApiSecret);
- Configure the edition list (e.g. BASIC/PRO/ULTIMATE) and the upgrade strategy (SAME_CODE = upgrade in place / NEW_CODE = issue a new code) in the publish form;
- Keep the secret on your software server; the client only keeps the license code and the activation token.
3. API Overview
Base path: https://www.powersoftware.app/frontApi (replace with your deployment domain for local / test environments).
| API | When to call | Authentication | Signature |
|---|---|---|---|
POST /license/software/generate |
Right after an in-software payment succeeds | None (signature verified) | Required |
POST /license/software/upgrade |
Right after a renewal / edition upgrade payment succeeds | None (signature verified) | Required |
POST /license/activate |
User first activation / switching to a new device | None | Not required |
POST /license/verify |
Every time the software starts | None | Not required |
POST /license/deactivate |
User unbinds the old device (Profile Center) | Login | Not required |
4. Signature Rules (required for generate / upgrade)
Payload: concatenate the following fields in this exact order with newline characters (\n). For missing optional fields use empty values (edition empty string, expiryDays = 0):
productId \n machineCode \n edition \n expiryDays \n clientOrderId \n licenseCode \n timestamp
Signature: HMAC-SHA256(Software API secret licenseApiSecret, payload), Base64URL-encoded, and put into the signature field.
Replay protection: timestamp is a millisecond timestamp. The platform requires it to be within 5 minutes of the server time, otherwise signatureInvalid is returned.
Node.js example:
const crypto = require("crypto");
const payload = [productId, machineCode, edition, expiryDays, clientOrderId, licenseCode, timestamp].join("\n");
const signature = crypto.createHmac("sha256", API_SECRET).update(payload).digest("base64url");
Python example:
import hashlib, hmac, base64
payload = "\n".join([str(productId), machineCode, edition, str(expiryDays), clientOrderId, licenseCode, str(timestamp)])
signature = base64.urlsafe_b64encode(hmac.new(API_SECRET.encode(), payload.encode(), hashlib.sha256).digest()).decode().rstrip("=")
(Base64URL is standard Base64 with + replaced by -, / replaced by _, and trailing = padding removed.)
5. API Details
5.1 Generate a License Code (call after in-software payment)
When to call: right after the user finishes an in-software payment, your server calls this once and stores the returned license code and activation token. The same order should be sent only once; repeated calls are idempotent and will not issue another code.
POST /license/software/generate
Request parameters (JSON body):
| Parameter | Type | Required | Source | Description |
|---|---|---|---|---|
| productId | number | Yes | Hard-coded / server config | Product ID, matching the product published on the platform |
| machineCode | string | Yes | Client-generated | Machine code of the current device, at least 8 characters |
| edition | string | No | Chosen by the client at payment | Edition ID; must be in the product edition list; defaults to the first edition of the product |
| expiryDays | number | No | Chosen by the client at payment | Validity in days, 0 = permanent; range 0-3650 |
| clientOrderId | string | Yes | Client / server-generated | In-software payment order ID, 1-64 characters; idempotency key |
| timestamp | number | Yes | Server-generated | Millisecond timestamp |
| signature | string | Yes | Server-computed | HMAC signature, at least 10 characters |
Response content fields:
| Field | Type | Description |
|---|---|---|
| licenseId | number | License record ID |
| licenseCode | string | The license code (write it into your license data) |
| productId | number | Product ID |
| edition | string | Edition ID |
| machineId | number | Bound machine record ID |
| activatedAt | string | Activation time (ISO8601) |
| expiryTime | string/null | Expiry time (ISO8601), null = permanent |
| trialExpiryTime | string/null | Trial expiry snapshot (ISO8601), null = not upgraded from a trial |
| trialExpiryTime | string/null | Trial expiry snapshot (ISO8601), null = not upgraded from a trial |
| trialExpiryTime | string/null | Trial expiry snapshot (ISO8601), null = not upgraded from a trial |
| trialExpiryTime | string/null | Trial expiry snapshot (ISO8601), null = not upgraded from a trial |
| maxMachines | number | Maximum number of bindable machines |
| activationToken | string | Activation token; store it for startup verification |
Response example:
{
"code": "SUCCESS",
"requestId": "xxx",
"success": true,
"content": {
"licenseId": 1001,
"licenseCode": "AB12-CD34-EF56-GH78",
"productId": 88,
"edition": "PRO",
"machineId": 2001,
"activatedAt": "2026-08-05T10:00:00.000Z",
"expiryTime": "2027-08-05T10:00:00.000Z",
"trialExpiryTime": "2026-12-01T10:00:00.000Z",
"trialExpiryTime": "2026-12-01T10:00:00.000Z",
"trialExpiryTime": "2026-12-01T10:00:00.000Z",
"trialExpiryTime": "2026-12-01T10:00:00.000Z",
"maxMachines": 1,
"activationToken": "eyJhbGciOiJIUzI1NiJ9..."
}
}
Error codes for this API: productIdRequired, machineCodeInvalid, orderIdRequired, productNotFound, productNotEnabled, apiSecretMissing, signatureInvalid, orderAlreadyUsed.
5.2 Upgrade Edition / Renew (call after payment)
When to call: after the user completes a renewal or an edition-upgrade payment. The same order should be sent only once.
POST /license/software/upgrade
Request parameters (JSON body):
| Parameter | Type | Required | Source | Description |
|---|---|---|---|---|
| productId | number | Yes | Hard-coded / server config | Product ID |
| licenseCode | string | Yes | Stored in the software | Current license code, at least 10 characters |
| machineCode | string | No | Client-generated | Current machine code; pass it when migrating bindings (NEW_CODE strategy) or binding an extra machine |
| edition | string | Yes | Chosen by the client at payment | Target edition ID |
| expiryDays | number | No | Chosen by the client at payment | Additional validity in days, 0 = permanent; range 0-3650 |
| clientOrderId | string | Yes | Client / server-generated | Order ID of this purchase; idempotency key |
| timestamp | number | Yes | Server-generated | Millisecond timestamp |
| signature | string | Yes | Server-computed | HMAC signature |
Handling strategy (determined by the product configuration):
- SAME_CODE (upgrade in place): the license code stays the same, machine bindings stay valid, and the validity is extended (a permanent license stays permanent). The response adds
upgraded: trueto the fields of 5.1; - NEW_CODE (issue a new code): the old code is revoked, a new code is issued, and machine bindings are migrated. The response additionally returns
oldLicenseId, andlicenseCodeis the new code.
Response content fields: same as 5.1, plus upgraded: true; under NEW_CODE the response also returns oldLicenseId (the old license record ID; the old code has been revoked).
Error codes for this API: productIdRequired, licenseCodeRequired, orderIdRequired, productNotFound, productNotEnabled, apiSecretMissing, signatureInvalid, codeNotFound, revoked, orderAlreadyUsed, editionRequired.
5.3 Activate a License (first activation / new device)
When to call: after the user obtains a license code (self-service issuance, admin issuance, etc.) and enters it on the activation page of the client software; call it on the new device when the user switches machines.
POST /license/activate
Request parameters (JSON body):
| Parameter | Type | Required | Source | Description |
|---|---|---|---|---|
| licenseCode | string | Yes | User input | License code, at least 10 characters |
| machineCode | string | Yes | Client-generated | Machine code of the current device, at least 8 characters |
Response content: same as 5.1 (licenseId, licenseCode, productId, edition, machineId, activatedAt, expiryTime, maxMachines, activationToken).
Notes:
- A license code can be bound to multiple machines, limited by
maxMachines(defaults to the product configuration); - Activating the same machine again reuses the existing binding and re-issues an activation token (idempotent);
- Failed activations are rate-limited per IP + license code; too many failures return
tooManyAttempts.
Error codes for this API: codeNotFound, revoked, expired, machineLimit, tooManyAttempts.
5.4 Verify a License (every software startup)
When to call: every time the software starts (or before unlocking features). The software must store the activationToken returned by 5.1 / 5.2 / 5.3.
POST /license/verify
Request parameters (JSON body):
| Parameter | Type | Required | Source | Description |
|---|---|---|---|---|
| licenseCode | string | Yes | Stored in the software | License code, at least 10 characters |
| machineCode | string | Yes | Client-generated | Machine code of the current device, at least 8 characters |
| activationToken | string | Yes | Issued by the platform, stored by the software | Activation token, at least 20 characters |
Response content fields:
| Field | Type | Description |
|---|---|---|
| valid | boolean | Always true (failures are returned as error codes) |
| licenseId | number | License record ID |
| productId | number | Product ID |
| edition | string | Current edition ID |
| expiryTime | string/null | Expiry time (ISO8601), null = permanent |
| trialExpiryTime | string/null | Trial expiry snapshot (ISO8601), null = not upgraded from a trial |
| trialExpiryTime | string/null | Trial expiry snapshot (ISO8601), null = not upgraded from a trial |
| trialExpiryTime | string/null | Trial expiry snapshot (ISO8601), null = not upgraded from a trial |
| trialExpiryTime | string/null | Trial expiry snapshot (ISO8601), null = not upgraded from a trial |
Notes:
- Verification results are cached for about 60 seconds; write operations (revoke, unbind, upgrade) invalidate the cache immediately, so there is no delay concern;
- The software should check
expiryTimeand stop unlocking features after expiry. - If the license originated from a trial, use
trialExpiryTimeas a grace window for top-tier features: keep them unlocked untiltrialExpiryTime. - If the license originated from a trial, use
trialExpiryTimeas a grace window for top-tier features: keep them unlocked untiltrialExpiryTime. - If the license originated from a trial, use
trialExpiryTimeas a grace window for top-tier features: keep them unlocked untiltrialExpiryTime. - If the license originated from a trial, use
trialExpiryTimeas a grace window for top-tier features: keep them unlocked untiltrialExpiryTime.
Error codes for this API: tokenInvalid, machineNotActivated, invalidOrRevoked, expired, tooManyAttempts.
5.5 Unbind a Device (before switching machines, login required)
When to call: the user unbinds the old device in "Profile Center → My Licenses", then activates on the new device via 5.3.
POST /license/deactivate
Request parameters (JSON body):
| Parameter | Type | Required | Source | Description |
|---|---|---|---|---|
| licenseCode | string | Yes | User license list | License code, at least 10 characters |
| machineCode | string | Yes | User license list | The machine code to unbind, at least 8 characters |
Response content: true.
Notes:
- Only the owner of the license (the buyer, or the user who registered the bound machine code) can operate;
- Unbind quota: by default the same license can be unbound only once within 30 days (configurable in the admin console); exceeding it returns
unbindQuotaExceeded.
Error codes for this API: noPermission, machineNotBound, unbindQuotaExceeded.
6. Error Codes
Business errors use a unified response (tip carries a human-readable message):
{
"code": "BUSINESS_WARNING",
"requestId": "xxx",
"success": false,
"errorMessage": "LICENSE_GENERATE_FAILED",
"tip": "Signature verification failed"
}
Parameter validation failures return code: "PARAM_VALIDATE_FAILED" and the specific field message in tip.
| Error code | Meaning |
|---|---|
productNotEnabled |
The product does not have license activation enabled |
apiSecretMissing |
The product has no Software API secret (licenseApiSecret) configured |
signatureInvalid |
Signature verification failed (wrong secret, wrong payload order, or timestamp out of window) |
machineCodeInvalid |
Invalid machine code (shorter than 8 characters) |
orderAlreadyUsed |
This order already generated a license code (idempotency conflict: replayed with a different machine code) |
codeNotFound |
License code not found |
revoked |
License code has been revoked or is unusable |
expired |
License code has expired |
invalidOrRevoked |
License is invalid or revoked during verification |
machineNotActivated |
This machine is not activated |
tokenInvalid |
Invalid activation token |
machineLimit |
The license has reached the maximum number of bound machines |
machineNotBound |
This machine is not bound |
noPermission |
No permission to operate on this license |
unbindQuotaExceeded |
The license can be unbound only once within 30 days; try again later |
tooManyAttempts |
Too many attempts; try again later |
productNotFound |
Product not found |
editionRequired |
Please select the target edition |
7. Typical Flows
- In-software payment loop: payment succeeds →
software/generate(returns the license code + activation token) → the software stores them → callverifyon every startup → unlock features; renewal / upgrade →software/upgrade; - Self-service / admin issuance: a license code is generated in the Developer Center or the admin console → the user gets the code →
activatein the client to bind the device and get the activation token →verifyon startup; - Switching machines: unbind the old device via
deactivatein the Profile Center →activateon the new device →verify; - Upgrade strategy difference: SAME_CODE keeps the same code; NEW_CODE issues a new code (the old code is revoked and machine bindings are migrated automatically).
8. Notes
- Keep the secret server-side only: signing must be done on your software server; if the client gets the secret, the license system is compromised;
- Machine code stability: use a stable machine fingerprint and hash it; if the machine code changes after reinstalling the OS, unbind first, then activate again;
- Idempotency: make
clientOrderIdglobally unique (e.g. timestamp + random), repeated requests will not issue duplicate codes; - Cache:
verifyresults are cached for about 60 seconds and invalidated immediately by write operations; - Rate limiting: failed activations / verifications are rate-limited; the client should show proper error messages and retry with delays;
- Trust the platform response: validity, edition, and machine limits are decided by the platform; do not relax them on the software side.
6. Trial claim & purchase page
For "Try first" client software products, the client calls POST /license/trial/claim after the free download.
Request parameters:
| Param | Type | Required | Description |
|---|---|---|---|
| productUniqueCode | string | yes | Product unique code (must be a Try-first product) |
| machineCode | string | yes | Machine code (>=8 chars; use the cross-language SDK algorithm) |
Success response is the same as 5.1 (licenseCode / activationToken / expiryTime = claim time + trial days). The response also carries trialExpiryTime (trial expiry snapshot, equal to expiryTime). The response also carries trialExpiryTime (trial expiry snapshot, equal to expiryTime). The response also carries trialExpiryTime (trial expiry snapshot, equal to expiryTime). The response also carries trialExpiryTime (trial expiry snapshot, equal to expiryTime).
The same product + machine returns the same trial license (idempotent).
After the trial, paid features require purchasing a license edition: when verification fails, prompt the user and open the purchase page with the machine code:
https://www.powersoftware.app/product/license/purchase?productUniqueCode={productUniqueCode}&machineCode={machineCode}
After payment, the platform issues a license (source=PLATFORM), shows it on the purchase page and emails it. SDK reference: ps-help/v3/doc/授权SDK规范_v3.md.
7. Official SDKs (Node / Python / Java)
Official SDKs in three languages (cross-language machine code, HMAC signing, 60s verification cache, purchase-page redirect) plus bilingual specs:
https://github.com/mizhanchengxi/powersoftware-license-sdk
8. Upgrade policy flag (licenseUpgradeMode)
The success response content of POST /license/activate, POST /license/verify and POST /license/trial/claim also returns the product-level upgrade policy:
| Field | Type | Description |
|---|---|---|
| licenseUpgradeMode | string | SAME_CODE = the license code stays the same after upgrade/renewal; NEW_CODE = a new code is issued (old code revoked) on upgrade/renewal |
Client usage:
- Decide whether to show a "bind license code" input: with
SAME_CODEthe code never changes, so never prompt users to re-enter it; withNEW_CODEa new code is issued on upgrade/renewal — overwrite the locally stored code with thelicenseCodereturned by the API; verifyreturns the latest value on every launch, so clients stay in sync.
Notes:
- Product-level configuration set by the developer in the publish form; defaults to
SAME_CODEwhen unset; verifyresults are cached for about 60s, so a platform-side config change takes effect within 60s;software/upgradealready returns the actual (old or new) code per policy and does not need this field.
9. Trial expiry snapshot (trialExpiryTime)
The success response content of POST /license/activate, POST /license/verify and POST /license/trial/claim also returns the trial expiry snapshot:
| Field | Type | Description |
|---|---|---|
| trialExpiryTime | string | null | Expiry time of the original trial license (ISO 8601); null = not upgraded from a trial (pure first purchase, developer-issued code, or trial not converted) |
How it is produced:
- Written when the trial is claimed, same value as the trial license's
expiryTime; - When purchasing after a trial (same-code upgrade or new-code reissue), the platform first reads the trial's original expiry time and freezes it into this field; after purchase
expiryTimebecomes the purchased edition's validity while this field stays unchanged; - Converting an expired trial also freezes the value (a past timestamp), so clients can detect "no grace left";
- Later paid upgrades/renewals (same code or new code) preserve this field.
Client usage (recommended): trial-first products unlock all features during the trial; after buying a lower edition, higher-tier features are locked immediately by edition gating. Clients may read trialExpiryTime and decide their own transition policy, e.g. keep granting a higher-tier feature while now < trialExpiryTime, then lock it and guide the user to pay the difference after that moment. Whether and how to use it is entirely up to the client; the platform does not enforce anything.
Note: when claiming a trial, if the machine already holds a non-trial license for the product (trial-to-purchase conversion, first purchase, developer-issued code, etc., i.e. already purchased), the API returns error code trialAlreadyPurchased and no additional trial license is issued; revoked (refunded) licenses do not count as purchased.