# Otwa Cloud API Reference

> Base URL: https://otwa.cloud/api/v1
> Authentication: Bearer token in `Authorization` header — every endpoint requires a scoped API key.
> Format: JSON
> OpenAPI spec: https://otwa.cloud/openapi.json
> Machine-readable feeds: `/openapi.json`, `/docs.md`, `/llms.txt`
> Current version: v1.9 (2026-04-24)

---

## Authentication

All requests require an API key generated from your dashboard:
https://otwa.cloud/dashboard/api

```
Authorization: Bearer YOUR_API_KEY
```

### Scopes

Each API key carries one or more scopes. Calls missing a required scope return **403 Forbidden**.

| Scope              | Grants                                                                         |
|--------------------|---------------------------------------------------------------------------------|
| `account:read`     | Read account info, balance, reseller state, product catalog, regions, OS list. |
| `servers:read`     | List servers, view server detail, credentials, live stats, add-on catalog.     |
| `servers:write`    | Deploy, rename, power-cycle, reset password, purchase add-ons, issue SSO.     |
| `servers:destroy`  | Permanently terminate a server. **Opt-in — not in the default scope set.**     |
| `billing:read`     | View balance and invoice history.                                              |

Separating `servers:destroy` from `servers:write` means a leaked deploy/automation key cannot delete infrastructure. Opt in explicitly when minting a key for a destructive integration.

---

## Catalog (unauthenticated info, read-scope-gated for API-key clients)

### GET /products
List all available server plans. Returns wholesale prices for reseller keys.
Requires: `account:read`

Response:
```json
[
  {
    "id": "uuid",
    "name": "Starter",
    "vcpu": 2,
    "ramMb": 4096,
    "diskGb": 50,
    "bandwidthGb": 1000,
    "monthlyPrice": "9.99",
    "regions": ["tr-ist-01"],
    "addons": [
      { "id": "vpk7wjf5", "name": "8 Additional IPs", "price": "32.00", "type": "extra-ip" }
    ]
  }
]
```

### GET /regions
Flat list of every deployment region currently offered.
Requires: `account:read`

Response:
```json
[
  { "slug": "tr-ist-01", "name": "Istanbul, Turkey" },
  { "slug": "eu-west-1", "name": "Amsterdam, Netherlands" }
]
```

### GET /os-templates
Every live OS family, flattened per version. Use the returned `id` as the `os` field on POST /servers.
Requires: `account:read`

Response:
```json
[
  { "id": "ubuntu-24.04",  "family": "Ubuntu",      "label": "Ubuntu 24.04 LTS" },
  { "id": "debian-13",     "family": "Debian",      "label": "Debian 13" },
  { "id": "rocky-10.1",    "family": "Rocky Linux", "label": "Rocky Linux 10.1" },
  { "id": "almalinux-10.1","family": "AlmaLinux",   "label": "AlmaLinux 10.1" },
  { "id": "windows-2025",  "family": "Windows",     "label": "Windows Server 2025" }
]
```

---

## Account

### GET /account
Authenticated account details.
Requires: `account:read`

Response:
```json
{
  "id": "a1b2c3d4-...",
  "email": "you@example.com",
  "firstName": "John",
  "lastName": "Doe",
  "balance": "50.00",
  "tier": "standard",
  "createdAt": "2026-01-01T00:00:00.000Z"
}
```

### GET /reseller
Reseller state of the authenticated account. Returns `null` for non-reseller accounts.
Requires: `account:read`

Response:
```json
{
  "isReseller": true,
  "brandSlug": "acme-hosting",
  "brandName": "Acme Hosting",
  "marginPercent": 20,
  "status": "active"
}
```

### POST /sso
Mints a 5-minute single-use login URL that signs the authenticated user into their dashboard.
Requires: `account:read`

Request body (optional):
```json
{ "next": "/dashboard/billing" }
```

Response:
```json
{
  "url": "https://otwa.cloud/sso?t=eyJhbGc...&next=%2Fdashboard%2Fbilling",
  "expiresIn": 300
}
```

---

## Servers

### GET /servers
All servers in your account, enriched with primary IP and additional IPs.
Requires: `servers:read`

Response:
```json
[
  {
    "id": "uuid",
    "label": "prod-1",
    "hostname": "srv1.example.com",
    "status": "running",
    "ipAddress": "185.17.112.10",
    "os": "ubuntu-24.04",
    "region": "tr-ist-01",
    "vcpu": 2,
    "ramMb": 4096,
    "diskGb": 50,
    "bandwidthGb": 1000,
    "monthlyPrice": "9.99",
    "createdAt": "2026-04-14T10:00:00.000Z",
    "additionalIps": [
      { "ip": "185.17.112.11", "gateway": "185.17.112.1", "netmask": "255.255.255.0", "cidr": "185.17.112.0/24" }
    ]
  }
]
```

### POST /servers
Deploy a new server. Deducts the monthly cost from your wallet balance. Goes through `pending` → `building` → `running` in ~60-120 seconds.
Requires: `servers:write`

Optional `Idempotency-Key` header: retries with the same key within 24 hours return the cached result instead of creating a duplicate server. Recommended for WHMCS and other auto-retry clients.

Request body (`productId`, `os`, `osTemplate` required; the rest optional):
```json
{
  "productId": "uuid",
  "os": "ubuntu-24.04",
  "osTemplate": "a45a4d5e-bcdf-4c5a-9b51-ec213c26b881",
  "label": "prod-api",
  "hostname": "api.example.com",
  "region": "tr-ist-01",
  "addons": ["vpk7wjf5"]
}
```

Response:
```json
{
  "id": "uuid",
  "label": "prod-api",
  "status": "pending",
  "os": "ubuntu-24.04",
  "region": "tr-ist-01",
  "vcpu": 2,
  "ramMb": 4096,
  "diskGb": 50,
  "monthlyPrice": "9.99",
  "createdAt": "2026-04-14T10:00:00.000Z"
}
```

### GET /servers/:id
Full server detail including structured `networking` and `specs` blocks. Poll while `status === 'pending'` or `'building'` to track provisioning.
Requires: `servers:read`

Response:
```json
{
  "id": "uuid",
  "label": "prod-api",
  "status": "running",
  "networking": {
    "primaryIp": "185.17.112.10",
    "gateway": "185.17.112.1",
    "additionalIps": [
      { "ip": "185.17.112.11", "gateway": "185.17.112.1", "netmask": "255.255.255.0", "cidr": "185.17.112.0/24", "region": "tr-ist-01" }
    ]
  },
  "specs": { "vcpu": 2, "ramMb": 4096, "ramGb": 4, "diskGb": 50, "bandwidthGb": 1000, "os": "ubuntu-24.04", "region": "tr-ist-01" }
}
```

### GET /servers/:id/credentials
Admin login credentials. Linux → root SSH; Windows → Administrator RDP. Only valid once the server is `running`.
Requires: `servers:read`

Response (Linux):
```json
{
  "username": "root",
  "password": "G7xK2mP9qR3s",
  "ip": "185.17.112.10",
  "connectionType": "ssh",
  "sshCommand": "ssh root@185.17.112.10"
}
```

Response (Windows):
```json
{
  "username": "Administrator",
  "password": "G7xK2mP9qR3s",
  "ip": "185.17.112.10",
  "connectionType": "rdp",
  "rdpAddress": "185.17.112.10:3389"
}
```

### GET /servers/:id/stats
Real-time VM metrics sampled from the hypervisor. Only available when the server is running and the guest agent is installed.
Requires: `servers:read`

Response:
```json
{
  "powerState": "POWERED_ON",
  "cpuCores": 2,
  "memoryMb": 4096,
  "hostname": "srv1",
  "guestOs": "Ubuntu Linux (64-bit)",
  "ip": "185.17.112.10",
  "toolsRunning": true,
  "totalDiskGb": 50,
  "nicCount": 1,
  "nicConnected": 1,
  "guestIps": ["185.17.112.10"],
  "live": {
    "cpuUsageMhz": 150, "cpuPct": 8, "hostCpuMhz": 2400, "numCpu": 2,
    "guestMemUsedMb": 1024, "totalMemMb": 4096, "memPct": 25,
    "uptimeSeconds": 3600,
    "diskReadKBps": 12, "diskWriteKBps": 4,
    "netRxKBps": 128, "netTxKBps": 64
  }
}
```

### PATCH /servers/:id/label
Rename a server.
Requires: `servers:write`

Request body:
```json
{ "label": "new-name" }
```

### POST /servers/:id/power/start
Power on a stopped server.
Requires: `servers:write`

### POST /servers/:id/power/stop
Gracefully power off a running server.
Requires: `servers:write`

### POST /servers/:id/power/reboot
Reboot a running server.
Requires: `servers:write`

### POST /servers/:id/reinstall
Wipe the disk and rebuild the server from a new OS template. **All data on the server is lost.** Preserves server ID, label, region, primary IPv4, and attached add-ons; billing is unchanged. A fresh admin password is generated — fetch it from `GET /servers/:id/credentials` once the server is running again.

Accepts an optional `Idempotency-Key` header; retries within 24 h return the cached response.

Requires: `servers:destroy` — same scope as termination, because the blast radius on the customer side is equivalent. Not included in the default scope set — grant explicitly.

Request body:
```json
{ "os": "ubuntu", "osTemplate": "ubuntu-24.04" }
```

Response:
```json
{ "message": "Reinstall queued" }
```

### POST /servers/:id/password-reset
Rotates the admin password (root on Linux, Administrator on Windows) live via the guest agent — no reboot. Returns the new password in the response. Requires a running server with the guest agent installed.
Requires: `servers:write`

Response (success):
```json
{ "success": true, "password": "nL4kT8wQ2zF7mR9v" }
```

Response (failure):
```json
{ "success": false, "message": "Guest agent is not responding" }
```

### POST /servers/:id/sso
Mints a 5-minute single-use login URL that drops the user directly on this server's detail page.
Requires: `servers:read`

Request body (optional):
```json
{ "next": "/dashboard/servers/uuid/console" }
```

Response:
```json
{ "url": "https://otwa.cloud/sso?t=eyJhbGc...&next=%2Fdashboard%2Fservers%2Fuuid", "expiresIn": 300 }
```

### GET /servers/:id/addons
Every add-on offered by the server's plan, with the prorated price to attach it today and whether an add-on of that type is already active. Only one add-on per type (`extra-ip`, `ip-class`, `backup`) can be active at a time.
Requires: `servers:read`

Response:
```json
{
  "addons": [
    { "id": "vpk7wjf5", "name": "8 Additional IPs", "type": "extra-ip", "price": 32, "proratedPrice": 15.47, "owned": false },
    { "id": "2fu2qedj", "name": "Monthly Full Backup", "type": "backup", "price": 69, "proratedPrice": 33.35, "owned": true }
  ],
  "daysRemaining": 14,
  "billingCycleEnd": "2026-05-01T12:00:00.000Z"
}
```

### POST /servers/:id/addons
Attach an add-on to a running server. The prorated amount is charged immediately; full monthly price is included in future renewals. For `extra-ip` / `ip-class` add-ons the new IPs are routed to the VM — you must configure them inside the guest OS yourself.
Requires: `servers:write`

Request body:
```json
{ "addonId": "vpk7wjf5" }
```

Response:
```json
{
  "addon": { "id": "vpk7wjf5", "name": "8 Additional IPs", "type": "extra-ip", "price": 32 },
  "prorated": 15.47,
  "daysRemaining": 14,
  "newMonthlyPrice": 41.99,
  "allocatedIpCount": 8
}
```

Returns `400` if an add-on of the same type is already active or your wallet balance is insufficient (wallet is refunded on allocation failure).

### DELETE /servers/:id
Permanently destroys the server, releases all IPs (including class blocks), and queues VM deletion on the hypervisor. **Irreversible.** API-key callers bypass the email/TOTP confirmation required in the web dashboard — the key itself is the proof of intent.
Requires: `servers:destroy` (opt-in scope, not in defaults)

Response:
```json
{ "success": true, "message": "Server has been permanently destroyed" }
```

---

## Server statuses

| Value        | Meaning                                                                    |
|--------------|-----------------------------------------------------------------------------|
| `pending`    | Order placed, waiting for the worker to pick up the provision job.          |
| `building`   | VM is being deployed, configured, and booted. Takes ~60-120s.                |
| `running`    | Server is online and accepting connections.                                  |
| `stopped`    | Powered off. Can be started via power API.                                   |
| `suspended`  | Billing issue — server inaccessible. Add funds to reactivate.                |
| `failed`     | Provisioning failed. Contact support or use admin retry.                     |
| `terminated` | Permanently destroyed. Data and IPs released.                                |

Typical flow: `pending → building → running` in ~60-120 seconds from order.

---

## Error codes

| Code | Meaning                                                            |
|------|---------------------------------------------------------------------|
| 400  | Bad Request — missing or invalid parameters                         |
| 401  | Unauthorized — missing or invalid API key                           |
| 402  | Payment Required — insufficient wallet balance                      |
| 403  | Forbidden — API key lacks the required scope                        |
| 404  | Not Found — resource does not exist or belongs to another account   |
| 429  | Too Many Requests — rate limit exceeded (100 req/min per key)       |
| 500  | Internal Server Error — contact support if persistent               |

All error bodies: `{ "message": "...", "statusCode": N }`

---

## Rate limits

**100 requests per minute per API key.** Responses include `X-RateLimit-Remaining` and `X-RateLimit-Reset` headers. Exceeded requests return 429 with a `Retry-After` header.

---

## Quick start

```bash
# 1. List your servers
curl https://otwa.cloud/api/v1/servers \
  -H "Authorization: Bearer YOUR_API_KEY"

# 2. Fetch a plan + OS template (scoped: account:read)
curl https://otwa.cloud/api/v1/products \
  -H "Authorization: Bearer YOUR_API_KEY"
curl https://otwa.cloud/api/v1/os-templates \
  -H "Authorization: Bearer YOUR_API_KEY"

# 3. Deploy a server (scoped: servers:write)
curl -X POST https://otwa.cloud/api/v1/servers \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"productId":"PLAN_ID","os":"ubuntu-24.04","osTemplate":"TEMPLATE_ID"}'

# 4. Get credentials (scoped: servers:read)
curl https://otwa.cloud/api/v1/servers/SERVER_ID/credentials \
  -H "Authorization: Bearer YOUR_API_KEY"

# 5. Terminate (scoped: servers:destroy — opt-in)
curl -X DELETE https://otwa.cloud/api/v1/servers/SERVER_ID \
  -H "Authorization: Bearer YOUR_API_KEY"
```

Generate your API key: https://otwa.cloud/dashboard/api
