# Developer Portal

This is the **single entry point** for integrating with WHost. Pick the
path that matches what you're trying to build:

| I want to… | Start here |
|------------|------------|
| Integrate WHost with any billing platform | [Quick start](#quick-start) → [PHP SDK](sdk-php.md) → `examples/03-billing-integration-pattern.php` in the [SDK archive](https://whost.wisecp.com/developer/downloads/whost-php-sdk.zip) |
| Receive lifecycle events instead of polling | [Quick start](#quick-start) → [Webhooks guide](webhooks.md) |
| Call the REST API directly (any language) | [Quick start](#quick-start) → [API reference](api-reference.md) → [HMAC signers](#2-compute-v2-hmac-signature) (5 languages) |
| Read every endpoint's schema | [`api-reference-generated.md`](api-reference-generated.md) (629 operations) |
| Understand the version stability promise | [`versioning-policy.md`](versioning-policy.md) |

---

## Quick Start

Every WHost API request needs four HMAC headers. Below is the canonical
payload format and a one-shot cURL probe — once it returns `200`, you
have a working integration foundation.

The API is served under the panel's own address:
`https://<panel-address>/api/v1/…`. The agent itself listens only on
`127.0.0.1:2000`; the panel's web server forwards `/api/v1/` requests to it.

### 1. Get credentials

In the admin panel:

```
Settings (gear icon in the top bar) → API Access → Create API Key
```

Pick the **Full Access** scope for a general integration — an **Audit
Read-Only** key reaches only `/api/v1/audit-logs`. Copy the **API Key** AND
the **API Secret** immediately — the secret is shown **once**. Restrict the
key with **Allowed IPs** (one IP or CIDR per line) if the consuming server
has a stable address.

### 2. Compute v2 HMAC signature

Canonical payload — every component newline-separated:

```
METHOD\nPATH\nTIMESTAMP\nNONCE\nBODY
```

- `METHOD` — uppercase HTTP verb (`GET`, `POST`, …)
- `PATH` — the request path exactly as sent, including the `/api/v1/...`
  prefix: not URL-decoded, and followed by `?` and the query string when
  the request has one (`/api/v1/accounts?limit=10`)
- `TIMESTAMP` — current Unix time, seconds (string); must be within 300 s
  of the server clock
- `NONCE` — 16–256 characters, **unique per request** (the examples use 32
  hex characters); a nonce already seen in the last 11 minutes is refused
- `BODY` — the exact request body bytes (empty for a request without a body)

`X-WHost-Signature` carries the lowercase hex HMAC-SHA256 of this payload,
keyed with the API secret.

Working signers in five languages — each one signs
`GET /api/v1/system/info`:

| Language | File |
|----------|------|
| PHP | [`examples/hmac-example.php`](examples/hmac-example.php) |
| JavaScript / Node.js | [`examples/hmac-example.js`](examples/hmac-example.js) |
| Go | [`examples/hmac-example.go`](examples/hmac-example.go) |
| Ruby | [`examples/hmac-example.rb`](examples/hmac-example.rb) |
| Bash / POSIX sh | [`examples/hmac-example.sh`](examples/hmac-example.sh) |

### 3. First request

```bash
# Using the Bash signer from the table above:
export WHOST_BASE_URL="https://panel.example.com"   # the panel address
export WHOST_API_KEY="..."
export WHOST_API_SECRET="..."
bash hmac-example.sh
# Expected: the system/info envelope — {"status":"success","data":{…}}
```

If the request is refused, the `error_code` in the response body names the
check that refused it (the Bash signer runs curl with `-f`, which hides the
body — the PHP, Node.js, Go and Ruby signers print the status and body):

- `401 AUTH_EXPIRED` — clock drift: `X-WHost-Timestamp` is more than 300 s
  away from the server clock. Run `timedatectl status` on the calling host.
- `401 AUTH_FAILED` — the key, the signature or the nonce was refused. The
  most common causes:
  - Body byte mismatch — sign exactly the bytes you send; a proxy on your
    side that recompresses or re-encodes the request body breaks the
    signature.
  - Wrong `PATH` — it must start with `/api/v1`, keep the exact case and
    any percent-encoding, and include the query string when there is one.
  - A nonce shorter than 16 characters, or one already used.
  - The signature sent in uppercase hex.
- `403 IP_NOT_ALLOWED` — the calling address is not in the key's Allowed IPs.
- `403 SCOPE_DENIED` — the key's scope does not cover the path.
- `429 AUTH_RATE_LIMITED` — 30 failed verifications from one address within
  5 minutes block that address for an hour.

### 4. Make it idempotent

Every mutating call (`POST` / `PUT` / `PATCH` / `DELETE`) accepts an
optional `X-Idempotency-Key` header — 8–64 characters of `A–Z a–z 0–9 _ -`
(anything else → `400 IDEMPOTENCY_KEY_INVALID`). Use a UUID v4 per logical
operation:

```bash
curl -X POST … \
     -H "X-Idempotency-Key: $(uuidgen)" \
     -d '{"username":"alice","password":"<account password>","domain":"alice.example.com","email":"alice@example.com","plan_id":"starter"}' \
     "$WHOST_BASE_URL/api/v1/accounts"
```

(`…` stands for `-H "Content-Type: application/json"` and the four
`X-WHost-*` headers from step 2, signed over the exact body string.)

Same key + same body within 24 h → the stored response is replayed
(`X-Idempotent-Replay: true`). 2xx and 4xx answers are stored; a 5xx answer
is not, so a retry after a 5xx runs the call again.
Same key + a different method, path or body → `409 IDEMPOTENCY_KEY_REUSED`
(caller bug). A key belongs to the API key (or session) that sent it: another
credential sending the same key runs a new request and never receives your
stored answer.

Full semantics: [`api-reference.md → Idempotency`](api-reference.md#idempotency).

### 5. Subscribe to lifecycle events

Polling for state changes? Don't — register a webhook:

```bash
curl -X POST … \
     -d '{
       "url": "https://your-system.example.com/whost-hook",
       "events": ["account.created","account.suspended","account.terminated","ssl.failed"],
       "description": "billing platform"
     }' \
     "$WHOST_BASE_URL/api/v1/system/webhooks"
```

Save the returned `secret` — it's used to HMAC-verify inbound payloads.
Full subscriber recipe (PHP SDK helper, signature formula, retry semantics,
SSRF guards): [`webhooks.md`](webhooks.md).

---

## Reference documents

| Topic | Document |
|-------|----------|
| Authentication, idempotency, pagination, rate limits, error catalog | [`api-reference.md`](api-reference.md) |
| Full per-endpoint schemas (629 operations) | [`api-reference-generated.md`](api-reference-generated.md) |
| Webhook event catalog (25 events), payload, headers, signing, retry | [`webhooks.md`](webhooks.md) |
| Version stability promise (no breaking change inside `/api/v1`; at least 15 months from a `/api/v2` announcement to v1 removal) | [`versioning-policy.md`](versioning-policy.md) |
| PHP SDK: download, install, what it covers | [`sdk-php.md`](sdk-php.md) |
| Python app hosting (Django, Flask, FastAPI runtime pipeline) | [`python-apps.md`](python-apps.md) |
| HMAC signer examples (5 languages) | [Quick Start → step 2](#2-compute-v2-hmac-signature) |

---

## Common integration patterns

### Pattern A — billing platform provisioning hook

For WISECP, WHMCS, Blesta and custom billing systems. Plain-PHP
reference: `examples/03-billing-integration-pattern.php` in the [SDK archive](https://whost.wisecp.com/developer/downloads/whost-php-sdk.zip).

```
order_activated  → POST   /api/v1/accounts                     (idempotency key: "order-create-$id")
order_suspended  → POST   /api/v1/accounts/{u}/suspend         (key: "order-suspend-$id")
order_resumed    → POST   /api/v1/accounts/{u}/unsuspend       (key: "order-resume-$id")
order_cancelled  → DELETE /api/v1/accounts/{u}                 (key: "order-terminate-$id")
package_changed  → POST   /api/v1/accounts/{u}/change-package  (key: "pkg-change-$changeId")
```

Idempotency keys mean network retries on timeout are safe — same logical
order will not double-provision.

### Pattern B — out-of-band billing reconciliation

Subscribe to the `account.` events (name each one — the `events` filter
takes exact names, not wildcards); your billing system stays in sync when
an operator manually suspends / re-activates via the panel.

```
account.suspended         → mark order "suspended" in billing
account.unsuspended       → flip back to "active"
account.terminated        → reconcile cancellation
account.package_changed   → recompute pricing
account.password_changed  → audit log entry only
```

### Pattern C — SSL certificate events

```
ssl.failed   → page on-call + open support ticket
ssl.renewed  → silence the open ticket (if any)
ssl.issued   → record the new validity window (data.extra.valid_to)
```

These events come from certificate requests and uploads made through the
panel or the API. Renewals that certbot runs on its own schedule send no
event, and not every automatic certificate request does (see the
[event catalog](webhooks.md#event-catalog)); to watch expiry, poll
`GET /api/v1/accounts/{username}/ssl/{domain}` (`valid_to`).

### Pattern D — backup verification

```
backup.completed → record success + size + timing (data.extra.size_mb)
backup.failed    → page on-call (operator's responsibility, not customer's)
```

A backup whose archive was created but whose remote upload failed arrives
as `backup.completed` with `data.extra.remote_upload_error` set.

### Pattern E — capacity planning via metrics

The `/api/v1/system/info` endpoint returns CPU / memory / disk usage.
Poll on a 5-minute cadence (within the read tier rate limit) and trend
in your own observability stack; `GET /api/v1/system/metrics?period=24h`
(`24h`, `7d` or `30d`) returns the history the agent samples every 15
minutes. WHost itself doesn't expose Prometheus metrics today.

---

## Stability & deprecation

`/api/v1/*` is **stable** — no breaking change ships under this prefix.
Backwards-incompatible work goes through `/api/v2/`, served in parallel
with v1: at least **15 months** pass between the v2 announcement and the
removal of v1 (a 3-month beta plus a deprecation window of at least 12
months).

This means an integration written today against `v1` keeps working
unchanged until v1's announced sunset date. Read
[`versioning-policy.md`](versioning-policy.md) for the full deprecation
timeline, header conventions, and how Sunset headers will surface when v2
is introduced.

---

## Support

| Channel | Use case |
|---------|----------|
| Support ticket (wisecp.com client area) | Bugs, integration questions, license issues, PHP SDK issues (name the SDK version from its `CHANGELOG.md`, [PHP SDK page](sdk-php.md)) — the main channel |
| `hello@wisecp.com` | The same, when you cannot open a ticket |
| Security disclosure | `security@wisecp.com` — responsible disclosure expected |

When reporting an integration bug, please include:
1. The full request (headers + body, with secrets redacted)
2. The full response (status, headers, body)
3. The agent version (`GET /api/v1/system/update/status` → `data.current_version`)
4. The SDK version, if using one
