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.
- Register the product at
https://api.shohost.com/admin/→ Products. The product id is the slug the client sends asproduct. Set name, files URL, trial days, trial cooldown days, max slots, and bind mode (ipordomain). Trial days0disables trials. - Issue paid keys with
POST /api/v1/licenses(bearer token from Admin → API keys) or from WHMCS viaPOST /issue.php. Give the returnedlicense_keyto the customer. Do not generate keys inside the product. - In the product, on first run with no key,
POST /egress.php, thenPOST /trial.php. - When the customer enters a key,
POST /activate.php. - Repeat trial or activate on a timer (Sho uses 5 minutes). The server does not push. The product must poll.
- Allow the product to run only when the JSON says
okis true andstatusisactive, andexpiresis null or still in the future. Any otherstatuslocks 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
- Discover the public address.
POST /egress.phpwith{}. Readbind_ip. That address is the license bind. Private LAN addresses are rejected. - First install with no key.
POST /trial.php. The server issues a trial lease bound to that public IP and/etc/machine-id. - Customer enters a paid key.
POST /activate.phpwith the same identity fields pluskey. - 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:
| File | Role |
|---|---|
/var/cpanel/shomr/license.json | Signed lease cache |
/var/cpanel/shomr/license.hmac_secret | Local HMAC secret |
/etc/machine-id | Machine identity sent as machine_id |
What the client must collect
Send these fields. Do not send passwords, account lists, or file contents.
| Field | Required | Source | Max stored |
|---|---|---|---|
product | yes | Product slug, shomultireseller | product id |
version | yes on trial and activate | Installed plugin version | not a bind field |
hostname | yes | socket.gethostname() | 255 |
machine_id | yes | First 128 characters of /etc/machine-id | 255 |
public_ip | yes | bind_ip from egress.php | public IPv4 or IPv6 only |
key | paid activate and refresh | Customer license key | see key size below |
domain | only if the product bind mode is domain | Site hostname | 255 |
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:
| Form | Where | Size |
|---|---|---|
trial | Value the plugin sends and stores | 4 characters, the word trial |
trial: + 16 hex | Internal row only, never shown to the plugin | 22 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 path | Auth | Purpose |
|---|---|---|
| GET /api/v1/products | bearer or WHMCS | Active products |
| GET /api/v1/products/{id} | none | Trial days, slot cap, files URL |
| GET /api/v1/products/{id}/commands | none | Install and uninstall commands. Optional license_key query |
| POST /api/v1/licenses | bearer | Issue a paid key. Body: product_id, optional billing_cycle, max_slots, reference_id, notes |
| GET /api/v1/licenses/{key} | bearer | Inspect one key and its activations |
| POST /api/v1/licenses/{key}/suspend | bearer | Status suspended |
| POST /api/v1/licenses/{key}/unsuspend | bearer | Status active |
| POST /api/v1/licenses/{key}/terminate | bearer | Status revoked |
| POST /api/v1/licenses/{key}/reissue | bearer | Clear activation slots, keep the same key |
| POST /api/v1/licenses/{key}/rotate | bearer | Delete the old key and return a new 24-character key |
| POST /api/v1/licenses/{key}/delete | bearer | Delete the key and its activations |
| POST /status.php | WHMCS api_key | Lookup by key or service_id |
| POST /issue.php | WHMCS api_key | Issue or sync a key from WHMCS |
| POST /revoke.php | WHMCS api_key | Suspend, unsuspend, or revoke |
| POST /commands.php | none | Install 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 mode | Trial | Paid key |
|---|---|---|
ip (default) | Bound to public IP and machine_id | Bound to the first public IP that checks in. max_slots is how many IPs may use one key. Default is 1. |
domain | Still bound to public IP and machine_id | Bound 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:
| Field | Meaning |
|---|---|
bind_mode | ip (server) or domain (website addon) |
product_group | server or website_addon (derived from bind_mode) |
install_path | When the product uses **License key only** delivery, path under the WHMCS root where the customer extracts the zip |
customer_instruction | Plain-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:
| Field | When set |
|---|---|
show_instructions | true = shell install commands; false = license key + download delivery |
customer_instruction | License-key-only mode: instruction text for the client area |
install_path | License-key-only mode: extract/upload path (no leading or trailing slash) |
install_trial, install_licensed, uninstall, extras | Install-instructions mode |
bind_mode, product_group | Binding 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
| status | Meaning |
|---|---|
| active | Lease is valid |
| invalid | Key not found |
| expired | Term ended |
| suspended | Admin or WHMCS suspend |
| revoked | Terminated |
| product_mismatch | Key belongs to another product |
| public_ip_required | No public bind address |
| machine_mismatch | Trial already tied to another machine id |
| machine_id_required | machine_id missing |
| slot_limit_reached | Paid key already used on the allowed number of servers |
| cooldown | Trial was used and the cooldown has not finished |
| trials_disabled | Product does not offer trials |
| unknown_product | product slug is not registered. Create it in the admin console before the client calls trial or activate |
| domain_required | Paid key is in domain bind mode and domain was not sent |