Sell and run our servers from your own website. The API lists the plans on sale at your prices,
quotes and places orders paid from your wallet, and lets you power, reinstall, rename and renew every server you own.
It is a REST API over HTTPS: resource-oriented URLs, JSON request bodies, JSON replies and standard HTTP status codes.
Every call acts as the account that owns the API key, and follows the same rules as the panel itself.
Send request bodies as JSON with Content-Type: application/json. Every reply is JSON in the same envelope:
success, an optional message on actions, and data.
Money
Numbers in rupees with up to 2 decimals, e.g. 424.8; currency is always INR
Dates
ISO 8601 with offset, e.g. 2026-11-03T10:00:14+05:30
Empty values
null when unknown or not given; some text fields that do not apply are an empty string
Timeouts
Allow 180 seconds on POST /orders, which creates servers during the call; 30 seconds elsewhere
HTTPS
Required. Plain HTTP is not supported
SUCCESS
{
"success": true,
"message": "Your server is ready.",
"data": {
"order": {
"reference": "AJORD-261003-70B8FC32",
"status": "active"
}
}
}
ERROR
{
"success": false,
"message": "This key can only read. Make a key with full access to order or change servers.",
"code": "read_only"
}
Errors
Branch your code on code, and show
message to people: it is written for them and says what to do. A message that says “Nothing was charged” means exactly that.
HTTP
Code
When
What to do
401
unauthenticated
No key, a malformed or revoked key, or a suspended account
Check the key; do not retry
402
insufficient_balance
The wallet does not cover the order or renewal
Top up, then retry
403
read_only
A read-only key called an action
Use a full key
403
ip_not_allowed
The key is limited to other IP addresses
Call from an allowed server
403
forbidden
The action is not allowed for this server
Show the message; do not retry
404
not_found
Unknown plan, or an order or server not on your account
Refresh the list or check the id
409
conflict
The state changed: an IP was taken, a window closed, the server is not active
Show the message; let the user choose again
409
in_progress
The same Idempotency-Key is still placing its order
Wait a minute, then repeat
422
invalid
A field is missing or out of range, or a coupon is not valid
Fix the request
429
rate_limited
More than 120 calls or 10 orders in a minute
Wait until the next minute
503
disabled
The API is switched off
Try later; contact support
502
unavailable
A network behind the plan or server did not answer
Retry after a few seconds, up to 3 times
500
server_error
An unexpected error on our side
Retry once; contact support with the time
Rate limits
Each key may make 120 calls and place 10 orders per minute. Over the limit the API answers
429 rate_limited; wait until the next minute.
Retry GET calls and quotes freely.
On 500, 502 or 503, wait a few seconds and retry up to 3 times, doubling the wait.
Never retry 4xx errors unchanged: fix the request first.
429 RESPONSE
{
"success": false,
"message": "Too many requests: this key may make 120 requests a minute. Try again in a minute.",
"code": "rate_limited"
}
Idempotency
Network failures happen. Send an Idempotency-Key header with every POST /orders,
using a unique value such as your own order number (8 to 80 letters, digits, dots, colons, dashes or underscores).
A repeat with the same key returns the first order with "repeated": true and charges nothing.
While the first call is still placing the order, a repeat answers 409 in_progress.
If the first call failed before anything was charged, the same key may be used again.
List endpoints take page (from 1) and perPage (1 to 100, default 25),
and reply with page, perPage and total.
Keep asking for the next page while page × perPage < total.
An order is charged when it is placed. Most orders come back active
from the create call itself; otherwise poll Retrieve an order every 30 to 60 seconds until it is
active or refunded.
Status
Meaning
What your code does
active
All servers are ready
Read each server's login and deliver it
paid
Paid, waiting to be set up
Poll
provisioning
Servers are being created
Poll
failed
An attempt failed; it is retried automatically
Poll; servers may already list some
review
Our team is checking it
Poll; contact support if it lasts hours
refunded
It could not be completed; the money is back in your wallet
Stop polling and tell your customer
Integration flow
A shop that sells servers through the API does five things:
List plans and cache them for up to 5 minutes; respect a plan's terms and maxQuantity.
There are no webhooks yet: poll orders for progress and servers for daysLeft, and renew before expiresAt.
There is no sandbox either: every order is real, so test with read-only keys and quotes, then one small order.
const { plans } = await sky('GET', '/plans?os=LINUX');
const { images } = await sky('GET', `/plans/${plans[0].id}/images`);
const order = { plan: plans[0].id, image: images[0].id };
const quote = await sky('POST', '/orders/quote', order);
if (quote.total > quote.balance) throw new Error('Top up the wallet first.');
let { order: placed } = await sky('POST', '/orders', order, { 'Idempotency-Key': 'order-1001' }, 180000);
while (!['active', 'refunded'].includes(placed.status)) {
await new Promise((r) => setTimeout(r, 30000));
({ order: placed } = await sky('GET', `/orders/${placed.reference}`));
}
for (const item of placed.servers) {
const { server } = await sky('GET', `/servers/${item.id}`);
console.log(server.ip, server.password);
}
plan = sky('GET', '/plans', params={'os': 'LINUX'})['plans'][0]
image = sky('GET', f"/plans/{plan['id']}/images")['images'][0]
order = {'plan': plan['id'], 'image': image['id']}
quote = sky('POST', '/orders/quote', order)
if quote['total'] > quote['balance']:
raise SystemExit('Top up the wallet first.')
placed = sky('POST', '/orders', order, headers={'Idempotency-Key': 'order-1001'}, timeout=180)['order']
while placed['status'] not in ('active', 'refunded'):
time.sleep(30)
placed = sky('GET', f"/orders/{placed['reference']}")['order']
for item in placed['servers']:
server = sky('GET', f"/servers/{item['id']}")['server']
print(server['ip'], server['password'])
Client code
Copy the client for your language into your project and set the SKY_KEY environment variable.
Each one wraps the reply envelope and turns failures into an exception that carries the HTTP status and code.
The examples under each endpoint call these clients: $api->call() in PHP and sky() in Node.js and Python.
Choose your language once and every example on the page follows.
CLIENT
# Keep the key in an environment variable on your server.
export SKY_KEY="ajv_your_key_here"
curl "https://www.munnavpswala.skyhostvps.com/api/v1/account" \
-H "Authorization: Bearer $SKY_KEY"
<?php
// SkyHostApi.php — PHP 7.4+ with the curl extension.
final class SkyHostApiException extends RuntimeException
{
public int $status;
public string $apiCode;
public function __construct(string $message, int $status, string $apiCode)
{
parent::__construct($message, $status);
$this->status = $status;
$this->apiCode = $apiCode;
}
}
final class SkyHostApi
{
private string $key;
private string $base;
public function __construct(string $key, string $base = 'https://www.munnavpswala.skyhostvps.com/api/v1')
{
$this->key = $key;
$this->base = rtrim($base, '/');
}
/** @return mixed the reply's "data" */
public function call(string $method, string $path, ?array $body = null, array $headers = [], int $timeout = 30)
{
$ch = curl_init($this->base . $path);
$headers[] = 'Authorization: Bearer ' . $this->key;
$headers[] = 'Accept: application/json';
if ($body !== null) {
$headers[] = 'Content-Type: application/json';
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode((object) $body));
}
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => $timeout,
CURLOPT_CONNECTTIMEOUT => 10,
]);
$raw = curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($raw === false) {
throw new SkyHostApiException('Could not reach the API: ' . $error, 0, 'network_error');
}
$reply = json_decode($raw, true);
if (!is_array($reply)) {
throw new SkyHostApiException('Unreadable reply (HTTP ' . $status . ').', $status, 'bad_reply');
}
if (empty($reply['success'])) {
throw new SkyHostApiException((string) ($reply['message'] ?? 'Request refused.'), $status, (string) ($reply['code'] ?? 'invalid'));
}
return $reply['data'];
}
}
// Usage
$api = new SkyHostApi(getenv('SKY_KEY'));
try {
$account = $api->call('GET', '/account');
echo 'Wallet: ' . $account['balance'] . PHP_EOL;
} catch (SkyHostApiException $e) {
echo $e->apiCode . ': ' . $e->getMessage() . PHP_EOL;
}
Plans on sale with your monthly price. Prices and stock are refreshed from each network every few minutes: cache the list for up to 5 minutes and quote before ordering.
Query parameters
osstringoptional
LINUX (default) or WINDOWS.
networkstringoptional
all (default) or a network id.
Response fields
plans[].idstring
The plan id for images, IPs, quotes and orders.
plans[].typestring
shared or dedicated resources.
plans[].ipSeriesstring
The block the IP comes from, e.g. 103.45.12 (shown as 103.45.12.xx).
plans[].locationstring
City or country where known, else empty.
plans[].coresinteger|null
vCPU cores.
plans[].ramMb / raminteger / string
Memory in MB and as a label.
plans[].monthlyPricenumber
Your price per month in rupees.
plans[].listPricenumber|null
For an approved seller, the list price per month beside your price; null for everyone else, and for a plan at the list price.
plans[].sellerDiscountobject|null
For an approved seller, where your price comes from: type, value and label. Type price is your own price for this plan, set for you, with value the price and labelyour price; nothing more comes off it. Otherwise type is percent or flat, value the percentage or the rupees off a month, and label what comes off, e.g. 20% off or ₹100 off a month. A flat amount never takes more than 90% off a price; on a plan where it would, the price is 90% off and the label says so, e.g. ₹1,000 off a month, limited to 90% off. Null for everyone else.
plans[].stockinteger|null
Servers left; null means in stock with no count.
plans[].ipChoosableboolean
True when the buyer may pick an exact IP.
plans[].termsinteger[]|null
The months this plan can be ordered for at once, e.g. [1, 3]; null means any from 1 to 36. Renewals use the same terms.
plans[].maxQuantityinteger|null
The most servers one order may ask for, from the network's limit and stock; null means up to 50.
plans[].notestring|null
The kind of server where a network sells more than one, such as Linux container or Virtual machine.
Free IPv4 addresses the buyer may choose from, for plans with ipChoosable: true. Other plans answer with an empty list and choosable: false. An address is checked again when you order.
Path parameters
planstringrequired
A plan id.
Response fields
stillLoadingboolean
True while the network is still listing its addresses; order without ip and one is chosen.
Charges your wallet and creates the servers during the call, so allow up to 180 seconds. Send an Idempotency-Key header: a repeat with the same key returns the first order with "repeated": true instead of buying again.
Headers
Idempotency-Keystringoptional
Your own unique order number, 8 to 80 letters, digits, dots, colons, dashes or underscores. Strongly recommended.
Body parameters
planstringrequired
A plan id from List plans.
imageintegerrequired
An image id from List plan images.
monthsintegeroptional
Billing term, 1 to 36. Default 1. Some plans sell only certain terms.
quantityintegeroptional
Number of servers, 1 to 50. Default 1. Some plans allow fewer at once.
ipstringoptional
An exact free IPv4 address from List free IPs. Only for plans with ipChoosable: true.
couponstringoptional
A coupon code. An invalid code is refused with its reason.
Response fields
referencestring
Our order number. Store it beside your own.
statusstring
paid, provisioning, active, failed, review or refunded. See Order statuses.
planstring
What was bought.
osstring
The operating system installed.
monthsinteger
Billing term.
quantityinteger
Number of servers.
totalnumber
Amount charged to your wallet, in rupees.
currencystring
Always INR.
createdAtstring
When the order was placed (ISO 8601).
messagestring|null
A short note while the order is not active, else null.
servers[]array
The servers created so far: id, name, ip, status, expiresAt.
Errors
402
insufficient_balance
The wallet does not cover the order; message says how much to add.
403
read_only
The key can only read.
409
conflict
The chosen IP was taken or the IP series sold out. Nothing was charged.
409
in_progress
The same Idempotency-Key is still placing its order.
422
invalid
A field is missing or out of range, or the coupon is not valid.
Usage now and a series of samples. time is a Unix timestamp in seconds. Disk usage reads 0 when the guest agent is not running; totalBytes is still correct.
Erases all data on the server and installs the image. On networks that set a new root password, the reply carries it in data.password; otherwise data is null and the current password stays valid.
Path parameters
idintegerrequired
The server id, from List servers or an order.
Body parameters
imageintegerrequired
An image id from List reinstall images.
Errors
403
forbidden
The image is not allowed for this plan, e.g. Windows on a Linux plan.
Where features lists password, send the new password. Where it lists newpassword, send an empty body and the network generates one, returned once in data.password. The old password stops working at once.
Path parameters
idintegerrequired
The server id, from List servers or an order.
Body parameters
passwordstringoptional
8 to 64 characters. Leave out to have one generated where supported.
Errors
422
invalid
Too short, or this server takes the other kind of reset.
A single-use console link that expires once used. Depending on the network you get a browser url, or a VNC-over-WebSocket wsUrl with its password for a noVNC client.
const data = await sky('POST', '/servers/450/console', {});
data = sky('POST', '/servers/450/console', {})
RESPONSE200 OK
{
"success": true,
"data": {
"url": null,
"wsUrl": "wss://console.example.com/api/ws/vnc?vncToken=…",
"password": "…",
"note": "This console link is single-use and expires as soon as it connects."
}
}
Quote a renewal
GET/api/v1/servers/{id}/renew/quote
Read-only or full key
Prices a renewal without renewing. coupon is null without a code, or carries APPLIED or INVALID with a message.
Path parameters
idintegerrequired
The server id, from List servers or an order.
Query parameters
monthsintegeroptional
Default 1. 1, 2, 3, 6 or 12 on most networks.
couponstringoptional
A coupon code.
Errors
422
invalid
A term this server does not renew for; message names the allowed ones.
The version is in the path. New fields and endpoints are added within
v1; a change that would break an existing integration would come as a new version.
Date
Version
Change
4 Oct 2026
1.2
Plans carry terms, maxQuantity and note; servers carry renewal; plan prices refresh every few minutes
3 Oct 2026
1.1
Order and renewal quotes, server login fields and features, password, rename, console and usage metrics