# MagicHost API v1

The API that billing systems (MagicBill) use to provision hosting on a MagicHost server: create an account, suspend
and unsuspend it, change its plan or password, read its usage, sign the customer in (SSO), and terminate it.

## The model

**One account = one customer login + one subscription** (the cPanel model). Every account has its own panel username and
password. The same email address may own many accounts. `customer_id` lets a second subscription use an existing
customer login when the billing system wants that.

## Connecting

- **Base URL:** `https://<panel hostname>/api/v1`. Tools & Settings > API Keys shows it.
- **Key:** made by the administrator in Tools & Settings > API Keys. It looks like `mhk_…` and is shown only once.
  - Limit the key to the billing server's IP addresses (IP or CIDR, one per line). A request from another IP is refused.
  - The key acts as the administrator who made it.
- **Header on every request:** `X-API-Key: mhk_…` (or `Authorization: Bearer mhk_…`).
- **Body:** JSON (`Content-Type: application/json`). Answers are JSON.
- **Success:** `{"success": true, …}` with HTTP 200 (201 when an account is created).
- **Failure:** `{"success": false, "error": "<text>", "code": <HTTP status>}`. The status is 401 for a wrong key or IP,
  404 when something is not found, and 409 or 422 when the request cannot be done. The error text is in the language
  of the key's administrator.
- **Fail2Ban:** wrong keys are logged as failed logins, and repeated failures ban the IP.

```bash
curl -H "X-API-Key: mhk_…" https://srv1.example.com/api/v1/ping
```

## Endpoints

### GET /ping
Checks the key and the connection.
```json
{"success": true, "api": "magichost", "version": "1.0", "hostname": "srv1.example.com", "server_type": "plesk", "time": "2026-10-02T15:16:30+00:00"}
```

### GET /plans
Returns the administrator's active hosting plans. A limit of `null` means unlimited.
```json
{"success": true, "plans": [{"id": 1, "name": "Starter", "slug": "starter", "description": "…", "web_hosting": true, "disk_mb": 2048, "traffic_mb": 25600,
  "domains": 1, "subdomains": 5, "databases": 2, "mailboxes": 5, "ftp_accounts": 1, "cpu": 1, "memory_mb": 256,
  "price_monthly": null, "price_yearly": null}]}
```
In other calls, `plan` may be the plan's `id`, `slug` or `name`.
`web_hosting: false` is a plan with mail and DNS only (no website, PHP, files, databases or FTP); an account on it is made
the same way, and changing its plan to one with web hosting sets up its website.

### POST /accounts — create an account
| field | | |
|---|---|---|
| `domain` | required | the subscription's main domain |
| `plan` | required | plan id, slug or name |
| `email` | required (new customer) | the customer's email (may repeat across accounts) |
| `name` | | contact name (default: the username) |
| `username` | | panel login; default: made from the domain (`a-z0-9_`, starts with a letter, 3–30 characters). Up to 16 letters/digits and free: also the account's system user (SFTP/SSH), as cPanel; the customer logs in with it |
| `password` | | panel password (the server's password policy applies); default: generated and returned once |
| `company`, `phone`, `language` (`en-US`, `el`), `country` (2 letters), `address`, `city`, `state`, `postal_code` | | customer details |
| `customer_id` | | give an existing customer instead of creating one (then `email`, `username`… are not used) |
| `reseller` | | a reseller's username: the new customer belongs to that reseller |

**201:**
```json
{"success": true, "account": {"id": 103, "domain": "example.com", "status": "creating", "customer_id": 57, "username": "example",
  "password": "Gk7…!", "plan": {"id": 1, "name": "Starter"}, "panel_url": "https://srv1.example.com", "job_id": 2240}}
```
- **`password`** is returned only when the panel generated it (the request had none). Otherwise it is `null`.
- **Creating takes a few seconds.** Poll `GET /accounts/{id}` until `status` is `active`.
- **On failure** (e.g. the domain already exists), a customer made for the account is removed again.

### GET /accounts/{id}
```json
{"success": true, "account": {"id": 103, "domain": "example.com", "status": "active", "suspend_reason": null,
  "plan": {"id": 1, "name": "Starter", "slug": "starter"},
  "customer": {"id": 57, "username": "example", "email": "client@mail.com", "name": "John Doe", "company": "", "phone": ""},
  "system_user": "example_com", "expires_at": null, "created_at": "2026-10-02 15:16:40", "ip": "203.0.113.10",
  "usage": {"disk_used_mb": 120, "disk_limit_mb": 2048, "traffic_used_mb": 340.5, "traffic_limit_mb": 25600}}}
```
- **`status`:** `creating`, `active`, `suspended` or `error`.
- **`traffic_used_mb`:** this month's traffic, counted from the 1st of the month (UTC).
- **A limit of `null`** means unlimited.

### GET /accounts?domain=…|username=…|email=…|customer_id=…
Finds accounts. Returns `{"success": true, "accounts": [ … ]}`, the same as above without `usage`.

### POST /accounts/{id}/suspend
Body: `{"reason": "Unpaid invoice #123"}` (optional). The websites, mail and FTP of the subscription stop. Returns the account.

### POST /accounts/{id}/unsuspend
Starts the account again. Returns the account. It is refused (409) while the subscription is over its disk or traffic
limit with a plan that does not allow overuse.

### POST /accounts/{id}/plan
Body: `{"plan": "business"}`. Changes the plan (upgrade or downgrade); the new limits apply at once. Returns the account.

### POST /accounts/{id}/password
Body: `{"password": "…"}`. Changes the customer's panel password (the password policy applies).

### POST /accounts/{id}/login — SSO
```json
{"success": true, "url": "https://srv1.example.com/sso?token=…", "expires_in": 300}
```
- **Redirect the customer to `url`.** It signs them in to their panel, opened at this subscription.
- **The link works once, within 5 minutes.** An expired or used link shows a page with a Log in link.

### DELETE /accounts/{id}[?keep_customer=1]
Removes the subscription: its websites, databases, mail, FTP and backups settings. The customer login is removed too,
unless it has other subscriptions or `keep_customer=1` is given. Returns `{"success": true, "job_id": …}`. The removal
runs in the background for a few seconds.

## A billing module's calls

| billing action | API |
|---|---|
| Test connection | `GET /ping` |
| Plans for products | `GET /plans` |
| CreateAccount | `POST /accounts`, then poll `GET /accounts/{id}` until `active` |
| Suspend / Unsuspend | `POST /accounts/{id}/suspend` / `unsuspend` |
| Terminate | `DELETE /accounts/{id}` |
| ChangePackage | `POST /accounts/{id}/plan` |
| ChangePassword | `POST /accounts/{id}/password` |
| Usage update | `GET /accounts/{id}` → `usage` |
| Login to panel (client area button) | `POST /accounts/{id}/login` → redirect to `url` |

Keep the account `id` that `POST /accounts` returns: all later calls use it. To find an account the billing system
lost, use `GET /accounts?domain=…`.
