← api.shohost.com

License API

ShoHost license API

Base URL: https://api.shohost.com

This page is the full contract. A new product implements licensing by calling only this host. Do not invent a second license server, a different key format, or a different bind rule.

Sho MultiReseller is one product on this API. Its slug is shomultireseller. Any other product uses its own slug, created first in the admin console.

Build a license into another product

Do these steps in order.

  1. Register the product at https://api.shohost.com/admin/ → Products. The product id is the slug the client sends as product. Set name, files URL, trial days, trial cooldown days, max slots, and bind mode (ip or domain). Trial days 0 disables trials.
  2. Issue paid keys with POST /api/v1/licenses (bearer token from Admin → API keys) or from WHMCS via POST /issue.php. Give the returned license_key to the customer. Do not generate keys inside the product.
  3. In the product, on first run with no key, POST /egress.php, then POST /trial.php.
  4. When the customer enters a key, POST /activate.php.
  5. Repeat trial or activate on a timer (Sho uses 5 minutes). The server does not push. The product must poll.
  6. Allow the product to run only when the JSON says ok is true and status is active, and expires is null or still in the future. Any other status locks the product. A network failure locks it until the next successful check.

Replace shomultireseller in the examples below with the new product id. The key prefix changes with that id. The URLs do not.

User-Agent should be the product name and version. ShoMultiReseller/<version> is only the Sho plugin. The server does not require that exact string.

Most denials are HTTP 200 with "ok": false and a status string. Do not treat HTTP 200 as success. Read ok and status. Unknown product on trial.php is HTTP 404. A missing key on activate.php is HTTP 400. A bad bearer token is HTTP 401.

All client calls are POST with Content-Type: application/json and User-Agent: ShoMultiReseller/<version>. Successful client replies are HMAC-SHA256 signed. The client checks signature before it saves the lease.

How the product calls the server

  1. Discover the public address. POST /egress.php with {}. Read bind_ip. That address is the license bind. Private LAN addresses are rejected.
  2. First install with no key. POST /trial.php. The server issues a trial lease bound to that public IP and /etc/machine-id.
  3. Customer enters a paid key. POST /activate.php with the same identity fields plus key.
  4. Keep the lease alive. Every 5 minutes the panel and cron repeat step 2 for a trial, or step 3 for a paid key. A denial (invalid, expired, revoked, suspended, machine_mismatch, slot_limit_reached) locks or clears the local lease. If the server cannot be reached, the panel stays locked until a later refresh succeeds.

Local files on the cPanel server:

FileRole
/var/cpanel/shomr/license.jsonSigned lease cache
/var/cpanel/shomr/license.hmac_secretLocal HMAC secret
/etc/machine-idMachine identity sent as machine_id

What the client must collect

Send these fields. Do not send passwords, account lists, or file contents.

FieldRequiredSourceMax stored
productyesProduct slug, shomultiresellerproduct id
versionyes on trial and activateInstalled plugin versionnot a bind field
hostnameyessocket.gethostname()255
machine_idyesFirst 128 characters of /etc/machine-id255
public_ipyesbind_ip from egress.phppublic IPv4 or IPv6 only
keypaid activate and refreshCustomer license keysee key size below
domainonly if the product bind mode is domainSite hostname255

public_ip may also be sent as egress_ip. The server prefers the IP it sees on the HTTP connection. It uses the reported public_ip only when the connection itself is a private hop (NAT or split DNS).

The server stores, per activation: license key, public IP, hostname, domain, bind value, machine id, first seen time, and last check-in time.

License key size

Paid keys are generated as:

{PREFIX}-{8 hex}-{8 hex}

- PREFIX is the first 6 letters and digits of the product id, uppercased. Characters other than A-Z, a-z, and 0-9 are removed first. If nothing remains, the prefix is SHO.

- For shomultireseller the prefix is SHOMUL (6 characters).

- A shorter product id makes a shorter prefix and a shorter key. acme becomes ACME-XXXXXXXX-XXXXXXXX (22 characters).

- Each hex group is 4 random bytes, hex-encoded and uppercased (8 characters, 0-9 and A-F).

- Two hyphens separate the three parts.

- Length formula: prefix length + 18. A 6-character prefix is always 24 characters.

For Sho MultiReseller the key is **24 characters**:

SHOMUL-XXXXXXXX-XXXXXXXX

Example shape: SHOMUL-A1B2C3D4-E5F60718.

Entropy is 8 random bytes (64 bits). The key is case-insensitive in practice because the server stores the uppercase form. Send it exactly as issued.

Trial keys are different:

FormWhereSize
trialValue the plugin sends and stores4 characters, the word trial
trial: + 16 hexInternal row only, never shown to the plugin22 characters

A trial refresh calls trial.php again. It does not send a paid key.

Client functions

These are the calls the installed product makes. No bearer token.

POST /egress.php

Body: {} or { "public_ip": "203.0.113.10" }.

Response fields: ok, connection_public_ip, remote_addr, reported_public_ip, bind_ip.

Use bind_ip as public_ip on the next call.

POST /trial.php

Claim or refresh a trial.

{
  "product": "shomultireseller",
  "version": "1.1.36",
  "hostname": "server.example.com",
  "machine_id": "contents-of-etc-machine-id",
  "public_ip": "203.0.113.10"
}

Success: ok, status active, type trial, key trial, license_key trial, expires (unix), expires_at, ip, signature, plus install command fields.

Failure status values: public_ip_required, cooldown, trials_disabled, machine_mismatch, unknown_product.

POST /activate.php

Activate or refresh a paid key. Same identity fields as trial, plus the key.

{
  "product": "shomultireseller",
  "version": "1.1.36",
  "hostname": "server.example.com",
  "machine_id": "contents-of-etc-machine-id",
  "public_ip": "203.0.113.10",
  "key": "SHOMUL-A1B2C3D4-E5F60718"
}

license_key is accepted as an alias of key.

Success: ok, status active, type paid, key, license_key, expires, expires_at, slots_used, max_slots, ip, signature.

Failure status values: invalid, expired, suspended, revoked, product_mismatch, public_ip_required, machine_mismatch, slot_limit_reached, machine_id_required.

POST /api/v1/verify

Same check as activate, for integrators. Body uses license_key (or key), product, public_ip, hostname, machine_id.

Returns valid true or false and the same status values. This route does not require a bearer token.

POST /api/v1/trials/claim

Same behavior as /trial.php. Body: product, public_ip, hostname, machine_id.

Management functions

These are for the license admin, WHMCS, and automation. They are not called by the cPanel plugin.

Bearer token: Authorization: Bearer <api key issued in Admin → API keys>.

WHMCS product list also accepts the WHMCS bridge key.

Method and pathAuthPurpose
GET /api/v1/productsbearer or WHMCSActive products
GET /api/v1/products/{id}noneTrial days, slot cap, files URL
GET /api/v1/products/{id}/commandsnoneInstall and uninstall commands. Optional license_key query
POST /api/v1/licensesbearerIssue a paid key. Body: product_id, optional billing_cycle, max_slots, reference_id, notes
GET /api/v1/licenses/{key}bearerInspect one key and its activations
POST /api/v1/licenses/{key}/suspendbearerStatus suspended
POST /api/v1/licenses/{key}/unsuspendbearerStatus active
POST /api/v1/licenses/{key}/terminatebearerStatus revoked
POST /api/v1/licenses/{key}/reissuebearerClear activation slots, keep the same key
POST /api/v1/licenses/{key}/rotatebearerDelete the old key and return a new 24-character key
POST /api/v1/licenses/{key}/deletebearerDelete the key and its activations
POST /status.phpWHMCS api_keyLookup by key or service_id
POST /issue.phpWHMCS api_keyIssue or sync a key from WHMCS
POST /revoke.phpWHMCS api_keySuspend, unsuspend, or revoke
POST /commands.phpnoneInstall commands for a product and optional key

Billing cycles understood by issue: monthly, quarterly, semiannually, annually, biennially, triennially, and lifetime (no expiry).

Bind rules

Product bind modeTrialPaid key
ip (default)Bound to public IP and machine_idBound to the first public IP that checks in. max_slots is how many IPs may use one key. Default is 1.
domainStill bound to public IP and machine_idBound to domain. activate.php and verify must send domain. Missing domain returns domain_required.

A second machine calling trial.php for the same product while a trial is active gets machine_mismatch. After the trial expires, a new trial waits trial_cooldown_days and returns cooldown with days_remaining.

expires: null means the paid key does not expire (Lifetime). Otherwise expires is a unix timestamp and expires_at is Y-m-d H:i:s in UTC.

Issue a paid key

POST /api/v1/licenses

Header: Authorization: Bearer <api key>

{
  "product_id": "yourproduct",
  "billing_cycle": "Annually",
  "max_slots": 1,
  "reference_id": "order-1001",
  "notes": "optional"
}

billing_cycle is one of Monthly (30 days), Quarterly (90), Semi-Annually (180), Annually (365), Biennially (730), Triennially (1095), Lifetime (no expiry). If omitted, the server uses Annually. term_days may be sent instead; 0 or empty means Lifetime.

Success HTTP 201:

{
  "ok": true,
  "license_key": "YOURPR-XXXXXXXX-XXXXXXXX",
  "product_id": "yourproduct",
  "type": "paid",
  "status": "active",
  "billing_cycle": "Annually",
  "expires_at": "2027-09-25 17:00:00",
  "max_slots": 1
}

Unknown product_id returns HTTP 400 and "error": "unknown_product".

Product catalog (WHMCS)

GET /api/v1/products returns active products. Each item includes:

FieldMeaning
bind_modeip (server) or domain (website addon)
product_groupserver or website_addon (derived from bind_mode)
install_pathWhen the product uses **License key only** delivery, path under the WHMCS root where the customer extracts the zip
customer_instructionPlain-text paragraph shown above the download (supports CUSTOMER-LICENSE-KEY substitution in commands APIs)

GET /api/v1/products/{id} includes the same delivery fields.

Install commands and website copy

GET /api/v1/products/{id}/commands?license_key=… and POST /commands.php return product_commands with:

FieldWhen set
show_instructionstrue = shell install commands; false = license key + download delivery
customer_instructionLicense-key-only mode: instruction text for the client area
install_pathLicense-key-only mode: extract/upload path (no leading or trailing slash)
install_trial, install_licensed, uninstall, extrasInstall-instructions mode
bind_mode, product_groupBinding and UI grouping

Configure license-key-only copy in **Admin → Products → What the customer sees → License key only**.

Signed response

trial.php, activate.php, issue.php, revoke.php, and status.php add signature. It is HMAC-SHA256 hex of the JSON object with keys sorted alphabetically and signature removed. Encoding is compact JSON (no extra spaces, slashes not escaped).

The HMAC secret is not on this page. A new product does not need it. Decide allow or lock from ok, status, and expires over HTTPS. Verify signature only if ShoHost has given that product its own client secret.

Status values

statusMeaning
activeLease is valid
invalidKey not found
expiredTerm ended
suspendedAdmin or WHMCS suspend
revokedTerminated
product_mismatchKey belongs to another product
public_ip_requiredNo public bind address
machine_mismatchTrial already tied to another machine id
machine_id_requiredmachine_id missing
slot_limit_reachedPaid key already used on the allowed number of servers
cooldownTrial was used and the cooldown has not finished
trials_disabledProduct does not offer trials
unknown_productproduct slug is not registered. Create it in the admin console before the client calls trial or activate
domain_requiredPaid key is in domain bind mode and domain was not sent