# HostOpy license API — developer & AI guide

**Canonical API host:** `https://api.hostopy.com` (client tries `api` first, then `ai.hostopy.com` if unreachable)  
**Admin UI:** `https://api.hostopy.com/admin/`  
**Database:** `hostopy_api` (products, licenses, activations, API keys, audit, settings)

Use this document when you add a product, wire a new PHP/plugin installer, or automate licenses without WHMCS.

---

## AI quick reference (paste into tasks)

```
Product slug = products.id (lowercase, e.g. hostopyResellerManager).
Admin: https://api.hostopy.com/admin/ → Products, Licenses, API keys, Settings.
Issue paid key without WHMCS: POST https://api.hostopy.com/api/v1/licenses
  Authorization: Bearer sk_live_...
  Body: {"product_id":"<slug>","billing_cycle":"Annually","max_slots":1}
Client install (server product): curl -fsSL <files_base_url>/install | bash -s -- --license <KEY>
Client trial: same install without --license; key returned is the literal word "trial".
Trial is always bound to server public IP + machine_id (/etc/machine-id).
Paid server product: bind_mode ip — slot = IP + machine_id (first machine wins).
Paid website addon: bind_mode domain — send domain on verify; trial still IP-bound.
Move license: admin Reissue or POST /api/v1/licenses/{key}/reissue (clears slots).
Legacy HMAC JSON: POST /trial.php and /activate.php (HostOpy ResellerManager installer uses these).
Modern JSON: POST /api/v1/trials/claim and POST /api/v1/verify (Bearer not required for these two).
```

---

## 1. Architecture

| Layer                                 | Role                                                                  |
| ------------------------------------- | --------------------------------------------------------------------- |
| **api.hostopy.com**                   | License REST API + admin console                                      |
| **files.\*** (e.g. files.hostopy.com) | Product tarball, `install` / `uninstall` scripts, `version.json` only |
| **WHMCS** (optional)                  | Billing; calls API with Bearer key to issue/suspend                   |

The installer on the customer server talks to **`/trial.php`** or **`/activate.php`** on the license host (HMAC-signed responses). Automation and WHMCS should prefer **`/api/v1/`**.

---

## 2. Admin setup (new product)

1. Open **Products** → **New product**.
2. Set **Product ID** (slug), name, optional **Files URL** (where `install` lives).
3. **Licensing:** trial days, cooldown days, slots per key.
4. **Where a paid license is bound:**
   - **Server (IP)** — cPanel/server plugins (HostOpy ResellerManager). Trial key = `trial`, bound to IP + `machine_id`.
   - **Website addon (domain)** — paid key bound to domain/subdomain; trial still `trial` on IP.
5. **Customer instructions:** license-key-only, or editable install commands (`CUSTOMER-LICENSE-KEY` placeholder).
6. **Settings** (global): billing cycle day counts, trial cleanup days, audit retention, admin password.

### API key for automation

**API keys** → generate → copy `sk_live_…` once.  
Use header: `Authorization: Bearer sk_live_…`

---

## 3. Issue and manage licenses (no WHMCS)

### Issue paid license

```http
POST https://api.hostopy.com/api/v1/licenses
Authorization: Bearer sk_live_xxxxxxxx
Content-Type: application/json

{
  "product_id": "hostopyResellerManager",
  "billing_cycle": "Annually",
  "max_slots": 1,
  "reference_id": "invoice-12345",
  "notes": "optional"
}
```

`billing_cycle`: `Monthly`, `Quarterly`, `Semi-Annually`, `Annually`, `Biennially`, `Triennially`, `Lifetime` (day counts in **Settings**).

Response includes `license_key`, `expires_at`, `max_slots`.

### Inspect

```http
GET https://api.hostopy.com/api/v1/licenses/{license_key}
Authorization: Bearer sk_live_...
```

### Lifecycle

| Action                               | Method                              |
| ------------------------------------ | ----------------------------------- |
| Suspend                              | `POST .../licenses/{key}/suspend`   |
| Unsuspend                            | `POST .../licenses/{key}/unsuspend` |
| Revoke                               | `POST .../licenses/{key}/terminate` |
| Clear IP/machine slots (move server) | `POST .../licenses/{key}/reissue`   |

---

## 4. Client runtime (on the customer server)

### Legacy endpoints (HostOpy ResellerManager installer today)

| Endpoint                                    | When                         |
| ------------------------------------------- | ---------------------------- |
| `POST https://api.hostopy.com/trial.php`    | Install without `--license`  |
| `POST https://api.hostopy.com/activate.php` | Install with `--license KEY` |

Example body (both):

```json
{
  "product": "hostopyResellerManager",
  "hostname": "server.example.com",
  "machine_id": "<contents of /etc/machine-id>",
  "key": "HOSTOPYMUL-...."
}
```

Trial response: `license_key` is **`trial`** (not a random id).  
Paid activation uses the real key in `key` / `license_key`.

### REST equivalents

```http
POST https://api.hostopy.com/api/v1/trials/claim
POST https://api.hostopy.com/api/v1/verify
```

**Verify body (paid, server product):**

```json
{
  "product_id": "hostopyResellerManager",
  "license_key": "HOSTOPYMUL-....",
  "machine_id": "abc...",
  "hostname": "cpanel.example.com"
}
```

**Verify body (website addon):**

```json
{
  "product_id": "my-wp-addon",
  "license_key": "....",
  "domain": "shop.example.com",
  "machine_id": "abc..."
}
```

**Client IP:** Trials and IP-bound licenses use the **public** IP of the HTTP connection (`CF-Connecting-IP`, `X-Forwarded-For`, or `REMOTE_ADDR`). Private/RFC1918 addresses are rejected (`public_ip_required`). The JSON `ip` field is **not** used for binding (it cannot be spoofed).

Common failure statuses: `invalid`, `expired`, `suspended`, `slot_limit_reached`, `machine_mismatch`, `domain_required`, `public_ip_required`, `cooldown` (trials).

---

## 5. Install commands (what customers run)

From product **Files URL** (e.g. `https://files.hostopy.com/hostopy-ResellerManager`):

```bash
# Trial (15 days if enabled on product)
curl -fsSL https://files.hostopy.com/hostopy-ResellerManager/install | bash

# Paid
curl -fsSL https://files.hostopy.com/hostopy-ResellerManager/install | bash -s -- --license YOUR-KEY
```

The install script calls the license host configured in the **client** (`HOSTOPY_LICENSE_HOST`, default `https://api.hostopy.com`).  
If activation fails with timeout, the server cannot reach that URL (firewall/DNS) — fix routing or set:

```bash
export HOSTOPY_LICENSE_HOST=https://api.hostopy.com
```

before install, or activate after install:

```bash
HOSTOPY_LICENSE_HOST=https://api.hostopy.com hostopyResellerManager-ctl --activate YOUR-KEY
```

**Important:** Published tarballs must include client code that sends **`machine_id`**. Without it, two VMs behind the same public IP can both activate until the package is rebuilt.

---

## 6. How often the client checks the license

There is **no background cron** that pings the API every hour.

| Situation                   | What happens                                                                                                                                                                                       |
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Active license**          | Status is read from local cache `/var/cpanel/hostopymr/license.json`. Expiry is checked **locally** using the `expires` timestamp on every **commercial write** (WHM plugin actions, hooks).       |
| **Network refresh**         | The API is called on **install** (trial or activate), when the admin uses **Activate** / **Claim trial** in WHM, or when `hostopyResellerManager-ctl --activate` / `--claim-trial` runs.           |
| **Offline grace**           | If the cache is older than **7 days** (`OFFLINE_GRACE_SEC`), the license can still work until `expires`, but `needs_refresh` is set — no automatic refresh until something triggers a server call. |
| **Expired (trial or paid)** | The next `ensure_license()` / UI gate fails **immediately** from local `expires` — no wait for API.                                                                                                |
| **Expired trial only**      | Client may call **trial.php** again (server applies **cooldown**; may return `cooldown` for 180 days by default).                                                                                  |
| **Expired paid**            | Does **not** auto-fall back to trial; customer must renew or enter a new key.                                                                                                                      |

To force a live check: `hostopyResellerManager-ctl --activate KEY` or `POST /activate.php` / `POST /api/v1/verify`.

---

## 7. Testing two servers (your lab)

1. Admin: expire trials or wait; issue one paid key with **max_slots = 1**.
2. Server A: install with `--license KEY` (must send `machine_id`).
3. Server B: same command → expect **`machine_mismatch`** or **`slot_limit_reached`**.
4. Trial during cooldown: `POST /trial.php` → **`cooldown`**.

Reissue the key in admin to move it to another machine.

---

## 8. Deploy note

PHP for the API lives under SFTP `api.hostopy.com/` on the HostOpy server.  
Do not commit `config.php` (DB password, HMAC secret).  
Mirror to `ai.hostopy.com` only if you still serve old DNS; **canonical** is **api.hostopy.com**.

---

## 9. Related files in this repo

| Path                             | Purpose                        |
| -------------------------------- | ------------------------------ |
| `license_server/`                | API + admin PHP                |
| `lib/hostopymr/license.py`       | HostOpy ResellerManager client |
| `docs/LICENSE_API_V1.md`         | Short endpoint table           |
| `docs/LICENSE_PLATFORM_SETUP.md` | WHMCS + infra overview         |
