Provisioning Servers
The eleven endpoints that define and test the servers services run on, and import existing accounts.
Overview
A server is where services actually get provisioned. These endpoints define one, test its connection, set what shows in the panel, and move accounts already sitting on the server into WISECP.
Credentials go one way: you write the password and the access key but no endpoint reads them back. The detail only shows whether they are set.
Reference
Listing the Servers
Returns the servers services are provisioned on.
active ya da inactive.curl 'https://panel.example.com/api/v1/admin/products/servers' \
-H "Authorization: Bearer $API_KEY"const res = await fetch('https://panel.example.com/api/v1/admin/products/servers', {
headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/products/servers');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey],
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);$response = Api::Products()->GetServers();Server Detail
Returns all of a server's settings. The password and access key never come back.
active ya da inactive.curl 'https://panel.example.com/api/v1/admin/products/servers/34' \
-H "Authorization: Bearer $API_KEY"const res = await fetch('https://panel.example.com/api/v1/admin/products/servers/34', {
headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/products/servers/34');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey],
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);$response = Api::Products()->GetServer(['id' => 34]);
// The password itself is absent; only whether one is set.
$ready = $response['data']['has_password'] || $response['data']['has_access_hash'];{
"data": {
"id": 34,
"name": "srv1.example.com",
"type": "cPanel",
"ip": "192.0.2.10",
"username": "root",
"has_password": true,
"has_access_hash": false,
"port": 2087,
"secure": true,
"nameservers": ["ns1.example.com", "ns2.example.com"],
"max_accounts": 200,
"full_alert": 0,
"cost": { "price": 0, "currency_id": 147 },
"status": "active",
"fields": { "api_url": "https://panel.example.com/" },
"disabled_features": null
}
}Adding a Server
Adds a new provisioning server.
has_password and has_access_hash, and module fields marked secret are masked.curl -X POST 'https://panel.example.com/api/v1/admin/products/servers' \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d '{"type":"cPanel","name":"srv1.example.com","ip":"192.0.2.10","username":"root","password":"secret","port":2087,"secure":true,"nameservers":["ns1.example.com","ns2.example.com"]}'const res = await fetch('https://panel.example.com/api/v1/admin/products/servers', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'cPanel',
name: 'srv1.example.com',
ip: '192.0.2.10',
username: 'root',
password: 'secret',
port: 2087,
secure: true,
}),
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/products/servers');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'type' => 'cPanel',
'name' => 'srv1.example.com',
'ip' => '192.0.2.10',
'username' => 'root',
'password' => $secret,
'port' => 2087,
'secure' => true,
]),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// Try the connection with the same body first: the test writes nothing.
$test = Api::Products()->TestServerConnection([
'type' => 'cPanel',
'ip' => '192.0.2.10',
'username' => 'root',
'password' => $secret,
]);
if ($test['data']['connected'] ?? false) {
Api::Products()->CreateServer([
'type' => 'cPanel',
'name' => 'srv1.example.com',
'ip' => '192.0.2.10',
'username' => 'root',
'password' => $secret,
]);
}Updating a Server
Applies the fields you send. Leave the password out and the current one is kept.
curl -X PATCH 'https://panel.example.com/api/v1/admin/products/servers/34' \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d '{"max_accounts":300,"secure":true}'const res = await fetch('https://panel.example.com/api/v1/admin/products/servers/34', {
method: 'PATCH',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ max_accounts: 300, secure: true }),
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/products/servers/34');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'PATCH',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['max_accounts' => 300, 'secure' => true]),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// Sending back the masked password from the detail is harmless: the current one is kept.
Api::Products()->UpdateServer([
'id' => 34,
'max_accounts' => 300,
]);Deleting a Server
Deletes the server. The delete is refused while it still carries live services.
gate:product.server_delete hook vetoed the operation.curl -X DELETE 'https://panel.example.com/api/v1/admin/products/servers/34' \
-H "Authorization: Bearer $API_KEY"const res = await fetch('https://panel.example.com/api/v1/admin/products/servers/34', {
method: 'DELETE',
headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/products/servers/34');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'DELETE',
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey],
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);$response = Api::Products()->DeleteServer(['id' => 34]);Changing Status in Bulk
Changes several servers' status. The whole list is validated before anything is written.
active ya da inactive.ids was empty.curl -X POST 'https://panel.example.com/api/v1/admin/products/servers/bulk' \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d '{"ids":[34,35],"action":"inactive"}'const res = await fetch('https://panel.example.com/api/v1/admin/products/servers/bulk', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ ids: [34, 35], action: 'inactive' }),
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/products/servers/bulk');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'ids' => [34, 35],
'action' => 'inactive',
]),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// If one server is refused, NONE of them change; there is no half-applied state.
$response = Api::Products()->BulkServers([
'ids' => [34, 35],
'action' => 'inactive',
]);Testing the Connection
Tries to reach the server with the details you give, and does nothing else.
curl -X POST 'https://panel.example.com/api/v1/admin/products/servers/test-connection' \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d '{"type":"cPanel","ip":"192.0.2.10","username":"root","password":"secret","port":2087}'const res = await fetch('https://panel.example.com/api/v1/admin/products/servers/test-connection', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'cPanel',
ip: '192.0.2.10',
username: 'root',
password: secret,
port: 2087,
}),
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/products/servers/test-connection');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'type' => 'cPanel',
'ip' => '192.0.2.10',
'username' => 'root',
'password' => $secret,
'port' => 2087,
]),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// The test does NOT try an existing server; it tries the details in the body.
// To check a stored server you send its details again.
$test = Api::Products()->TestServerConnection([
'type' => 'cPanel',
'ip' => '192.0.2.10',
'username' => 'root',
'password' => $secret,
]);Switching Panel Features Off
Sets which tools and cards stay hidden from the client on this server's services.
tools holds tool names, cards card names, and card_items the rows to hide per card.curl -X PATCH 'https://panel.example.com/api/v1/admin/products/servers/34/preferences' \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d '{"disabled_features":{"tools":["backup"],"cards":[],"card_items":{}}}'const res = await fetch('https://panel.example.com/api/v1/admin/products/servers/34/preferences', {
method: 'PATCH',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
disabled_features: { tools: ['backup'], cards: [], card_items: {} },
}),
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/products/servers/34/preferences');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'PATCH',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'disabled_features' => [
'tools' => ['backup'],
'cards' => [],
'card_items' => new stdClass(),
],
]),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);$response = Api::Products()->SetServerPreferences([
'id' => 34,
'disabled_features' => [
'tools' => ['backup'],
'cards' => [],
'card_items' => [],
],
]);Signing In to the Server Panel
Produces a sign-in link for the server's own control panel.
curl -X POST 'https://panel.example.com/api/v1/admin/products/servers/34/sso' \
-H "Authorization: Bearer $API_KEY"const res = await fetch('https://panel.example.com/api/v1/admin/products/servers/34/sso', {
method: 'POST',
headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/products/servers/34/sso');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey],
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// The link opens the server's ADMIN panel, not a client account.
$response = Api::Products()->GetServerSso(['id' => 34]);
$url = $response['data']['login_url'] ?? null;Listing the Importable Accounts
Returns the accounts that exist on the server but have no counterpart in WISECP.
curl -G 'https://panel.example.com/api/v1/admin/products/servers/34/importable' \
-H "Authorization: Bearer $API_KEY" \
-d limit=200const url = new URL('https://panel.example.com/api/v1/admin/products/servers/34/importable');
url.searchParams.set('limit', '200');
const res = await fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
const body = await res.json();$url = 'https://panel.example.com/api/v1/admin/products/servers/34/importable?' . http_build_query(['limit' => 200]);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey],
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);$accounts = Api::Products()->GetServerImportable(['id' => 34], ['limit' => 200]);
// 'method' names the listing path used; whether paging works depends on it.
$paged = ($accounts['meta']['method'] ?? '') === 'list';Importing the Accounts
Turns the server's accounts into WISECP services. Each row is tied to a client and a product.
items was empty.curl -X POST 'https://panel.example.com/api/v1/admin/products/servers/34/import' \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d '{
"items": [
{
"info": { "username": "acct1", "domain": "client-domain.example" },
"user_id": 42,
"product_id": 15,
"price_id": 88,
"start": "2026-01-01 00:00:00"
}
]
}'const res = await fetch('https://panel.example.com/api/v1/admin/products/servers/34/import', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
items: [
{
info: account,
user_id: 42,
product_id: 15,
price_id: 88,
start: '2026-01-01 00:00:00',
},
],
}),
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/products/servers/34/import');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'items' => [[
'info' => $account,
'user_id' => 42,
'product_id' => 15,
'price_id' => 88,
'start' => '2026-01-01 00:00:00',
]],
]),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// 'info' goes back exactly as the listing endpoint gave it; do not reshape it.
$accounts = Api::Products()->GetServerImportable(['id' => 34])['data'];
$items = [];
foreach ($accounts as $account)
$items[] = [
'info' => $account,
'user_id' => 42,
'product_id' => 15,
'price_id' => 88,
'start' => '2026-01-01 00:00:00',
];
$done = Api::Products()->ImportServerAccounts(['id' => 34, 'items' => $items]);
// Compare what you sent with what came back: rows with a gap were dropped.
$dropped = count($items) - count($done['data']['imported']);Pitfalls
The password and the access key appear in no response; the detail only shows whether they are set. The module's secret-marked fields come back masked with asterisks. Sending that mask back on an update is harmless: the current value is kept and the mask is not stored.
The test endpoint tries the details in your body and writes nothing. To check whether a stored server still answers you have to send its details again, and since you cannot read the password back, you must be holding it on your side.
The bulk status change validates the whole list first and writes afterwards. If a server is still tied to a product or a live service the request is refused and no server changes. The error detail names which server and which tie blocked it.
Rows missing a client, product or price id are skipped and do not appear in the response. If all of them are missing you get no_valid_items, but if only some are, the request looks successful. Compare how many rows you sent with how many services came back.
Related Articles
Thanks for your feedback!
Our support team is here around the clock for anything you can't find above.