# Hosting

> Buy and provision shared hosting asynchronously: pick a package, create the account, poll its status, and look it up later.

Buy and provision shared hosting accounts. Unlike everything else in this API, hosting creation is **asynchronous**: you get HTTP 202 immediately and poll for the result.

## The flow

1. `GET /api/v1/hostings/packages` — pick a `servicePlan` value.
2. `POST /api/v1/hostings/create` — returns **202** with a `serviceId` and status `Provisioning`.
3. `GET /api/v1/hostings/check-status` — poll until `Active` or `Failed`. When `Active`, the credentials are in the response.

:::note[Why it is asynchronous]
Creating an account on the hosting panel takes 20 to 60 seconds on its own. Rather than hold your HTTP request open that long, we take the order, charge your balance, and hand the provisioning to a background worker. If provisioning then fails on every attempt, **your balance is refunded automatically**.
:::

:::caution[Hosting errors do not carry error codes]
Every other area of this API returns a machine-readable `code`. The hosting endpoints currently return a plain English `message` only, with **HTTP 403** and `error.code` empty. Until that changes, match on `error.message` for these endpoints — the exact strings are listed with each endpoint below.
:::

## GET /api/v1/hostings/packages

Lists what you can sell. Call this first — the `servicePlan` value from here is what `create` expects.

**Request — no parameters**

```http
GET /api/v1/hostings/packages
```

**Response — 200**

```json
[
  {
    "name": "Starter",
    "requiredMinLeasing": 1,
    "permittedMaxLeasing": 12,
    "servicePlans": [
      {
        "servicePlan": "starter-linux-01",
        "operatingSystem": "Linux",
        "panelType": "cPanel"
      },
      {
        "servicePlan": "starter-win-01",
        "operatingSystem": "Windows",
        "panelType": "Plesk"
      }
    ]
  },
  {
    "name": "Business",
    "requiredMinLeasing": 1,
    "permittedMaxLeasing": 24,
    "servicePlans": [
      {
        "servicePlan": "business-linux-01",
        "operatingSystem": "Linux",
        "panelType": "cPanel"
      }
    ]
  }
]
```

| Field | How to use it |
| --- | --- |
| `name` | Package name, for display. |
| `requiredMinLeasing` | Minimum lease term in months. |
| `permittedMaxLeasing` | Maximum lease term in months. |
| `servicePlans[].servicePlan` | **The exact value to send to `create`.** Do not construct it yourself. |
| `servicePlans[].operatingSystem` | `Linux` or `Windows`. |
| `servicePlans[].panelType` | `cPanel` or `Plesk`. |

:::caution[The operating system belongs to the plan, not the package]
One package can offer both a Linux/cPanel and a Windows/Plesk variant, as in the `Starter` example above. Let the customer choose deliberately — **this cannot be changed after provisioning.** Moving between them means a new account and a manual migration.
:::

## POST /api/v1/hostings/create

Buys the package and queues provisioning. **Returns HTTP 202, not 200.**

**Request**

```json
{
  "domainName": "apiname.com",
  "servicePlan": "starter-linux-01"
}
```

| Field | Required | Notes |
| --- | --- | --- |
| `domainName` | required | The domain the account is created for. Must be a valid registrable domain — it does not have to be registered with us. |
| `servicePlan` | required | A `servicePlan` value from `GET /packages`. |

**Response — 202 Accepted**

```jsonc
{
  "serviceId": "HST-2026-0004821",
  "status": "Provisioning",
  "panelAddress": "https://panel.apiname.com",
  "username": "",
  "password": "",
  "token": ""
}
```

:::danger[The credential fields are empty here — that is expected]
`username`, `password` and `token` exist on the response but are **always empty at this stage**, and `status` is **always `Provisioning`**. The account does not exist on the panel yet.

**Store `serviceId`.** It identifies the hosting in every other call, and it is the only thing you get from this response that you cannot obtain again easily.
:::

:::tip[What to do next]
Poll `GET /api/v1/hostings/check-status` with the `serviceId`. Show the customer a "setting up your hosting" state in your UI rather than a credentials page.
:::

**Errors — messages, no codes**

| Message | Meaning and fix |
| --- | --- |
| `The provided domain name is invalid.` | `domainName` could not be parsed as a domain. |
| `No product is defined for package '{name}'.` | Configuration problem on our side. Open a support ticket. |
| `No price defined for package '{name}'.` | No price set for your account on that package. Open a ticket. |
| `Insufficient balance to purchase this hosting package.` | Top up and retry. |

:::note[If a hosting stays in `Provisioning` for a long time]
Normal provisioning finishes in under two minutes. If a record is still `Provisioning` after, say, 30 minutes, something went wrong in a way that did not surface as an error. Report it with the `serviceId` rather than creating a second hosting — that would charge you twice.
:::

## GET /api/v1/hostings/check-status

Reports where provisioning got to and, once complete, returns the credentials.

**Request — send exactly one of these**

| Parameter | Notes |
| --- | --- |
| `serviceId` | From the `create` response. |
| `domainName` | Alternative lookup key. |

```http
GET /api/v1/hostings/check-status?serviceId=HST-2026-0004821
```

:::caution[Exactly one — not both, not neither]
Sending both `serviceId` and `domainName` is an error, as is sending neither. This applies to `GET /hostings` as well.
:::

**Response — still provisioning**

```jsonc
{
  "serviceId": "HST-2026-0004821",
  "domainName": "apiname.com",
  "status": "Provisioning",
  "statusMessage": "The hosting account is being created on the server.",
  "panelAddress": "https://panel.apiname.com",
  "username": "",
  "password": "",
  "token": "",
  "failureReason": null
}
```

**Response — ready**

```jsonc
{
  "serviceId": "HST-2026-0004821",
  "domainName": "apiname.com",
  "status": "Active",
  "statusMessage": "The hosting account is ready.",
  "panelAddress": "https://panel.apiname.com",
  "username": "apiname1",
  "password": "xK7#mQ2pL9vR4tZ8",
  "token": "a4f81c2e9b7d35604e8a1f93c62b07d5",
  "failureReason": null
}
```

**Response — failed**

```json
{
  "serviceId": "HST-2026-0004821",
  "domainName": "apiname.com",
  "status": "Failed",
  "statusMessage": "The hosting account could not be created. The amount has been refunded.",
  "panelAddress": "",
  "username": "",
  "password": "",
  "token": "",
  "failureReason": "Account name already exists on the target server"
}
```

**Status values**

`status` is always a **string** — the name below, never a number.

| `status` | What to do |
| --- | --- |
| `Provisioning` | Keep polling. Expect 20–60 seconds, occasionally longer. |
| `Active` | Ready. Read `username`, `password` and `token`. |
| `Failed` | **Your balance has already been refunded.** Read `failureReason`, fix the cause, call `create` again. |
| `Suspended` | Suspended because the lease expired. Renew to reactivate; if it is not renewed by the deletion date it becomes `Deleted`. |
| `Deleted` | Terminal. |

:::danger[Credentials are returned only while the status is `Active`]
`username`, `password` and `token` are populated **only** when `status` is `Active`. In every other state they are empty strings.

Capture them on the first `Active` response and store them encrypted. The response is sent with `Cache-Control: no-store` for a reason — do not log it, do not put it in a URL, and do not cache it in a browser.
:::

:::tip[Polling advice]
This call costs 10 quota points, so polling every second will build up points fast. Poll every **5 to 10 seconds** for the first two minutes, then back off. Give up and report the `serviceId` after about 30 minutes.
:::

**Errors — messages, no codes**

| Message | Meaning and fix |
| --- | --- |
| `Either serviceId or domainName must be provided.` | You sent neither. |
| `Provide only one of serviceId or domainName, not both.` | You sent both. |
| `Hosting not found.` | No matching record. |
| `You are not authorized to access this hosting.` | The record belongs to another reseller. |

## GET /api/v1/hostings · GET /api/v1/hostings/by-token · GET /api/v1/hostings/plans

Look up a hosting record, or read its plan limits.

**GET /api/v1/hostings — by service ID or domain**

```http
GET /api/v1/hostings?serviceId=HST-2026-0004821

{
  "hostingId": "b7f24e90-3a51-4c86-9d02-6e8b1f5a7c34",
  "serviceId": "HST-2026-0004821",
  "domainName": "apiname.com",
  "packageName": "Starter",
  "leaseOn": "2026-10-30T16:55:12",
  "expiredOn": "2027-10-30T16:55:12",
  "status": "Active",
  "externalCustomerId": "CUST-88201"
}
```

:::caution[Two different identifiers]
`serviceId` (`HST-…`) is what you use for `check-status` and lookups. `hostingId` (a GUID) is what `GET /plans` expects. **They are not interchangeable.** Store both.

No credentials are returned here — use `check-status` for those.
:::

**GET /api/v1/hostings/by-token — when all you have is the panel token**

```http
GET /api/v1/hostings/by-token?token=a4f81c2e9b7d35604e8a1f93c62b07d5
```

Returns the same object. Useful when a provisioned account's token is the only reference you have.

**GET /api/v1/hostings/plans — resource limits**

```http
GET /api/v1/hostings/plans?hostingId=b7f24e90-3a51-4c86-9d02-6e8b1f5a7c34

[
  {
    "name": "Starter Linux",
    "externalId": "starter-linux-01",
    "diskQuotaMb": 10240,
    "bandwidthLimitMb": 102400,
    "maxDomains": 1,
    "maxSubDomains": 10,
    "maxDatabases": 5,
    "maxFtpAccounts": 3,
    "maxEmailAccounts": 25,
    "hasDedicatedIp": false,
    "hasShellAccess": false
  }
]
```

:::note[`null` means unlimited, not zero]
A `null` numeric limit means the resource is unlimited on that plan. Do not render it as "0" — that is the opposite of what it means.
:::

**Errors — messages, no codes**

| Message | Meaning and fix |
| --- | --- |
| `Hosting not found.` | No matching record. |
| `You are not authorized to access this hosting.` | Belongs to another reseller. |
| `Token is required.` | `token` empty on `/by-token`. |
| `Token not recognized.` | No hosting matches that token. |
| `Token was not recognized by the panel.` | The hosting panel rejected the token. |
| `The account for this token could not be resolved.` | The panel returned no account. |
| `Account information could not be retrieved from the panel: {detail}` | Panel error; the detail is theirs. |
| `No hosting account is available for this hosting.` | There is no panel account — provisioning never completed. Check `check-status`. |
