# PHP SDK

**Package:** `wisecp/whost-php-sdk` 0.1.1 — a download from this site
**Download:** [`whost-php-sdk.zip`](https://whost.wisecp.com/developer/downloads/whost-php-sdk.zip)
· SHA-256: [`whost-php-sdk.zip.sha256`](https://whost.wisecp.com/developer/downloads/whost-php-sdk.zip.sha256)
**Requires:** PHP 7.4 or newer with the `curl` and `json` extensions

The SDK is a PHP client for the WHost admin API. It is not tied to one
billing system: WISECP, WHMCS, Blesta and your own code use the same
package. It signs every request, offers an idempotency key on every write
call, retries on request, maps error answers to typed exceptions and
verifies incoming webhooks.

The package is not published on Packagist or GitHub; the archive on this
site is the way to get it.

---

## Download and install

```bash
curl -O https://whost.wisecp.com/developer/downloads/whost-php-sdk.zip
curl -O https://whost.wisecp.com/developer/downloads/whost-php-sdk.zip.sha256
sha256sum -c whost-php-sdk.zip.sha256
unzip whost-php-sdk.zip
```

The archive holds one folder, `whost-php-sdk/`: the SDK (`src/`), four
examples, a README, the CHANGELOG, the LICENSE and a `vendor/` folder with
the SDK's dependencies (Guzzle 7 and the PSR HTTP interfaces, chosen to run
on PHP 7.4 and newer).

**Licence.** The LICENSE lets you use, copy and modify the SDK, its examples
included, and distribute it as part of your own integration that connects to
WHost (for example a billing-system module) — not on its own. Keep the
LICENSE file and the copyright notices in every copy; anything else needs
written permission from WISECP LLC.

**Without Composer** — load the bundled autoloader:

```php
require __DIR__ . '/whost-php-sdk/vendor/autoload.php';
```

**With Composer** — if your project already has its own `vendor/`, delete
the bundled `whost-php-sdk/vendor/` folder, point a path repository at the
extracted folder and require the package; Composer then installs the
dependencies itself:

```json
{
  "repositories": [
    { "type": "path", "url": "./whost-php-sdk", "options": { "symlink": false } }
  ],
  "require": {
    "wisecp/whost-php-sdk": "^0.1"
  }
}
```

Run `composer update wisecp/whost-php-sdk` afterwards. Without the path
repository `composer require wisecp/whost-php-sdk` does not find the
package.

---

## API key

Create a key in the WHost admin panel under **Settings → API Access** and
limit it to the addresses your integration calls from. The secret is shown
once, when the key is created; keep it outside your code (an environment
variable or a secrets store).

A key created there is a server-wide key and reaches the admin API the SDK
calls. A key that a reseller or customer creates in the client panel is
bound to that account and reaches only the client API
(`/api/v1/client/...`); the SDK's resources answer `403 SCOPE_DENIED` for
it.

---

## Quickstart

```php
<?php
require __DIR__ . '/whost-php-sdk/vendor/autoload.php';

use WHost\WHostClient;
use WHost\Exceptions\WHostException;

$whost = new WHostClient(
    (string) getenv('WHOST_API_KEY'),
    (string) getenv('WHOST_API_SECRET'),
    'https://panel.example.com',   // the address you open the WHost panel on
    ['timeout' => 30.0]
);

try {
    $page = $whost->accounts->list(['limit' => 25]);
    foreach ($page['accounts'] as $account) {
        echo $account['username'], "\t", $account['domain'], PHP_EOL;
    }

    $account = $whost->accounts->create([
        'username' => 'demo01',
        'domain'   => 'demo01.example.com',
        'email'    => 'owner@example.com',
        'password' => $password,   // plain text over TLS; the agent hashes it itself
        'plan_id'  => $planId,     // the "id" of a plan from $whost->plans->list()
    ], 'order-12345');             // idempotency key, see below

    echo 'Created: ', $account['username'], PHP_EOL;
} catch (WHostException $e) {
    fwrite(STDERR, sprintf(
        "[%d %s] %s\n",
        $e->getHttpStatus(),
        $e->getErrorCode(),
        $e->getMessage()
    ));
    exit(1);
}
```

- **Base URL:** the panel's address; the client appends `/api/v1` itself.
  The WHost agent listens only on the server's loopback interface, so
  requests reach it through the panel's web server.
- **TLS:** certificate verification is on by default (`verify_ssl`); turn
  it off only for a test server with a self-signed certificate.
- **Answers:** each method returns the `data` field of the agent's answer as
  an array.

---

## What the SDK covers

One accessor per area of the admin API, 38 in all: accounts, domains, DNS,
SSL, e-mail, databases, FTP, files, cron, backups, PHP, plans, Python and
Node.js applications, firewall, Fail2Ban, WAF, spam filter, mail queue and
sent mail, logs, metrics, system and services, updates, licence, migration,
plugins, reseller permissions and white label. The method list of each
accessor is in `src/Resources/` of the archive; an IDE completes it from
there.

**Only a signed-in panel session reaches these, not an API key:** API key
management (`apiKeys`), the administrator's own profile (`profile`), the
administrator's notification centre (`notifications`), the panel sign-in
calls (`auth`) and the white-label settings. Over an API key they answer
`401 AUTH_FAILED`. Changing an account's password
(`accounts->changePassword()`) or the server's root password is refused for
API keys too (`403 HMAC_FORBIDDEN_FOR_CREDENTIAL_MUTATION`); the account
owner changes the password in the panel.

**No helper method yet:** webhook endpoint management
(`/api/v1/system/webhooks/...`), the client API (`/api/v1/client/...`) and
some newer admin endpoints. Call them through the low-level client with the
path after `/api/v1`:

```php
$events = $whost->http()->request('GET', '/system/webhooks/events');
// request(string $method, string $path, ?array $body = null,
//         array $query = [], ?string $idempotencyKey = null)
```

Every endpoint is listed in the [endpoint catalogue](api-summary.md).

---

## Idempotency

Write methods take an optional idempotency key as their last argument: the
second for `create($data, $key)`, the third where the method also takes a
data array, as in `suspend($username, $data, $key)`. A key is 8–64
characters of letters, digits, `-` and `_`; use one key per business event,
for example `order-12345` for provisioning order 12345.

The agent keeps the answer under that key for 24 hours:

| Request | Answer |
|---------|--------|
| Same key, same method, path and body | The stored answer, header `X-Idempotent-Replay: true`; the action does not run again. A secret shown once (API key or webhook secret, 2FA material, one-time sign-in link, OAuth token) comes back as `"***"` |
| Same key, different method, path or body | `409 IDEMPOTENCY_KEY_REUSED` |
| Same key from another API key | A new request; answers are kept per API key |
| Malformed key | `400 IDEMPOTENCY_KEY_INVALID` |
| An earlier attempt ended in a server error (5xx) | Not stored — the retry runs the action |

---

## Retries

```php
$whost = $whost->withRetry(3);
```

retries a request up to three more times on `429`, on `5xx` and on
connection errors, waiting 1 s, 2 s and 5 s. Send an idempotency key with
every write call you retry.

---

## Errors

| Class | When |
|-------|------|
| `AuthException` | HTTP 401 or 403, or an `error_code` that starts with `AUTH_` |
| `NotFoundException` | HTTP 404 |
| `RateLimitException` | HTTP 429 or `RATE_LIMIT_EXCEEDED`; `getRetryAfter()` gives the wait in seconds when the agent sends one |
| `ValidationException` | HTTP 400 or 422, or `VALIDATION_ERROR`; `getFieldErrors()` lists the fields |
| `WHostException` | Anything else: other 4xx and 5xx answers, and connection errors (`TRANSPORT_ERROR`) |

All of them are `WHost\Exceptions\WHostException` subclasses and carry
`getHttpStatus()`, `getErrorCode()`, `getDetails()` and `getRawBody()`. The
meaning of each error code is in the [error code
catalogue](api-reference.md#appendix-a-error-codes).

---

## Request signing

The SDK signs every request with HMAC-SHA256 and sends `X-WHost-Key`,
`X-WHost-Timestamp`, `X-WHost-Nonce` and `X-WHost-Signature`. The signed
text is

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

where `PATH` is the full request path including `/api/v1` and the query
string. The agent rejects a timestamp more than 300 seconds away from its
own clock, so keep the calling server's clock synchronised (NTP).

---

## Webhook verification

```php
use WHost\Http\WebhookVerifier;
use WHost\Exceptions\WHostException;

try {
    $event = WebhookVerifier::parse(
        file_get_contents('php://input'),
        $_SERVER['HTTP_X_WHOST_WEBHOOK_SIGNATURE'] ?? '',
        $_SERVER['HTTP_X_WHOST_WEBHOOK_TIMESTAMP'] ?? '',
        $endpointSecret
    );
} catch (WHostException $e) {
    http_response_code(401);   // WEBHOOK_SIGNATURE_INVALID, WEBHOOK_TIMESTAMP_OUT_OF_RANGE, ...
    exit;
}

switch ($event['event']) {
    case 'account.created':   /* ... */ break;
    case 'account.suspended': /* ... */ break;
    case 'ssl.failed':        /* ... */ break;
}
```

The verifier checks `X-WHost-Webhook-Signature` (`sha256=<hex>` over
`"{timestamp}.{raw body}"`) against the raw request body and rejects a
delivery older than 300 seconds. The agent makes up to five delivery
attempts and retries any answer other than 2xx, so drop duplicates by
`X-WHost-Webhook-Event-Id`. Events and payloads: [Webhooks](webhooks.md).

---

## Examples in the archive

| File | Shows |
|------|-------|
| `examples/01-quickstart.php` | List accounts, create one, read it back |
| `examples/02-account-lifecycle.php` | Create → suspend → unsuspend → delete |
| `examples/03-billing-integration-pattern.php` | The order hooks of a billing system: activate, suspend, resume, terminate, change plan |
| `examples/04-webhook-receiver.php` | A webhook endpoint with signature check and duplicate filter |

Run them from the extracted folder with `WHOST_API_KEY`,
`WHOST_API_SECRET` and `WHOST_BASE_URL` set. They create accounts on the
server they point at; use a test server.

---

## Why is there no WHMCS, Blesta or WISECP module?

The SDK is deliberately billing-agnostic: the glue to a billing system
lives on the billing side. `examples/03-billing-integration-pattern.php`
shows the hooks such a module needs. If you maintain a module that uses
this SDK, tell us at `hello@wisecp.com`.

---

## Versions and status

The SDK has its own version number, independent of the WHost version.
While it is `0.x`, a minor release may change behaviour; the `CHANGELOG.md`
in the archive says what changed. The SDK is part of the WHost beta; report
problems with a support ticket from your wisecp.com client area (or
`hello@wisecp.com` when you cannot open one).
