Provisioning Servers

7 views Markdown

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

get/api/v1/admin/products/servers
Products/GetServers admin paged

Returns the servers services are provisioned on.

Query parameters 4
searchstringSearches the name and the address.
groupintFilters by server group.
pageintDefaults to 1.
limitintDefaults to 25, maximum 100.
Response fields data[] — 6
idintServer id.
namestringThe server name.
typestringThe server module attached.
ipstringThe server address.
statusstringactive ya da inactive.
max_accountsintThe most accounts that can be provisioned.
Meta 4
totalintTotal records matching the filter.
pageintThe page you are on.
limitintThe page size.
next_pageintThe next page. Zero means you are on the last one.
Errors 1
insufficient_scope403The key lacks the required scope.
Request
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

get/api/v1/admin/products/servers/{id}
Products/GetServer admin no secrets returned

Returns all of a server's settings. The password and access key never come back.

Response fields data — 16
idintServer id.
namestringThe server name.
typestringThe server module attached.
ipstringThe server address.
usernamestringThe connection username.
has_passwordboolWhether a password is set. The password itself is never returned.
has_access_hashboolWhether an access key is set.
portintThe connection port.
secureboolWhether the connection is encrypted.
nameserversstring[]The server's nameservers. Up to four; empty ones are dropped.
max_accountsintThe most accounts that can be provisioned.
full_alertintThe threshold at which the capacity warning fires.
costobjectWhat the server costs you.
pricefloatWhat the server costs you.
currency_idintCurrency id.
statusstringactive ya da inactive.
fieldsobjectThe module's own settings. The fields depend entirely on the module; the ones marked secret come back masked with asterisks.
disabled_featuresobject | nullThe features switched off in the panel. Empty when none are.
Errors 2
not_found404No such server.
insufficient_scope403The key lacks the required scope.
Request
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'];
Response
{
  "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

post/api/v1/admin/products/servers
Products/CreateServer admin 201

Adds a new provisioning server.

Body 13
typestringrequiredName of the server module.
namestringrequiredThe server name.
ipstringrequiredThe server address.
usernamestringrequiredThe connection username.
passwordstringThe password. Either this or an access key is needed; it is stored encrypted.
access_hashstringThe access key. It stands in for the password.
portintThe connection port.
secureboolEncrypts the connection.
max_accountsintThe most accounts that can be provisioned. Defaults to 200.
full_alertintThe threshold at which the capacity warning fires.
nameserversstring[]The nameservers. Up to four.
costobjectWhat the server costs you.
pricefloatWhat the server costs you.
currency_idintCurrency id.
fieldsobjectThe module's own settings.
Response fields 201 — data
dataobjectThe server created. Same shape as the detail endpoint, so no secret comes back: the password and the access key are reported as has_password and has_access_hash, and module fields marked secret are masked.
Errors 6
name_required422The server name was empty.
type_required422The server type was empty.
ip_required422The address was empty.
username_required422The username was empty.
credentials_required422Neither a password nor an access key was given.
insufficient_scope403The key lacks the required scope.
Request
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

patch/api/v1/admin/products/servers/{id}
Products/UpdateServer admin a type change migrates

Applies the fields you send. Leave the password out and the current one is kept.

Body 13
typestringrequiredName of the server module.
namestringrequiredThe server name.
ipstringrequiredThe server address.
usernamestringrequiredThe connection username.
passwordstringThe password. Either this or an access key is needed; it is stored encrypted.
access_hashstringThe access key. It stands in for the password.
portintThe connection port.
secureboolEncrypts the connection.
max_accountsintThe most accounts that can be provisioned. Defaults to 200.
full_alertintThe threshold at which the capacity warning fires.
nameserversstring[]The nameservers. Up to four.
costobjectWhat the server costs you.
pricefloatWhat the server costs you.
currency_idintCurrency id.
fieldsobjectThe module's own settings.
Response fields data
dataobjectThe server as it now stands. Same shape as the detail endpoint and equally secret-free.
Errors 7
not_found404No such server.
name_required422The server name was empty.
type_required422The server type was empty.
ip_required422The address was empty.
username_required422The username was empty.
credentials_required422Neither a password nor an access key was given.
insufficient_scope403The key lacks the required scope.
Request
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

delete/api/v1/admin/products/servers/{id}
Products/DeleteServer admin refused while in use

Deletes the server. The delete is refused while it still carries live services.

Response fields data — 2
deletedboolWhether the delete succeeded.
idintId of the deleted server.
Errors 4
not_found404No such server.
server_in_use422The server still carries live services.
blocked_by_gate422The gate:product.server_delete hook vetoed the operation.
insufficient_scope403The key lacks the required scope.
Request
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

post/api/v1/admin/products/servers/bulk
Products/BulkServers admin all or nothing

Changes several servers' status. The whole list is validated before anything is written.

Body 2
idsint[]requiredThe server ids.
actionstringrequiredactive ya da inactive.
Response fields data — 2
updatedint[]The ids whose status changed.
actionstringThe status that was applied.
Errors 4
ids_required422ids was empty.
invalid_action422The action is neither of the two values.
server_in_use422A server is still tied to products or live services. The error detail names which server and which tie blocked it.
insufficient_scope403The key lacks the required scope.
Request
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

post/api/v1/admin/products/servers/test-connection
Products/TestServerConnection admin writes nothing

Tries to reach the server with the details you give, and does nothing else.

Body 13
typestringrequiredName of the server module.
namestringrequiredThe server name.
ipstringrequiredThe server address.
usernamestringrequiredThe connection username.
passwordstringThe password. Either this or an access key is needed; it is stored encrypted.
access_hashstringThe access key. It stands in for the password.
portintThe connection port.
secureboolEncrypts the connection.
max_accountsintThe most accounts that can be provisioned. Defaults to 200.
full_alertintThe threshold at which the capacity warning fires.
nameserversstring[]The nameservers. Up to four.
costobjectWhat the server costs you.
pricefloatWhat the server costs you.
currency_idintCurrency id.
fieldsobjectThe module's own settings.
Response fields data — 2
connectedboolWhether the server answered.
auto_fillobjectSettings the module read from the server and suggests. Not every module returns them.
Errors 4
type_required422The server type was empty.
ip_required422The address was empty.
invalid_type422The server module was not found.
test_failed422The connection could not be made.
Request
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

patch/api/v1/admin/products/servers/{id}/preferences
Products/SetServerPreferences admin

Sets which tools and cards stay hidden from the client on this server's services.

Body 1
disabled_featuresobjectrequiredWhat to switch off: tools holds tool names, cards card names, and card_items the rows to hide per card.
Response fields data — 2
idintServer id.
disabled_featuresobjectThe setting that was stored.
Errors 2
not_found404No such server.
insufficient_scope403The key lacks the required scope.
Request
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

post/api/v1/admin/products/servers/{id}/sso
Products/GetServerSso admin module dependent

Produces a sign-in link for the server's own control panel.

Body
No body is needed, send an empty one. The server comes from the path and the credentials from its stored record.
Response fields data — 1
login_urlstringThe sign-in link carrying the session.
Errors 4
not_found404No such server.
not_supported422The module does not support panel sign-in.
sso_failed422The sign-in link could not be produced.
insufficient_scope403The key lacks the required scope.
Request
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

get/api/v1/admin/products/servers/{id}/importable
Products/GetServerImportable admin schema comes from the module

Returns the accounts that exist on the server but have no counterpart in WISECP.

Query parameters 3
searchstringSearches the accounts.
pageintThe page. Works only on modules that can paginate.
limitintThe page size. Maximum 200.
Response fields data[]
data[]objectEach row is the raw account record the module read from the server. The fields depend on the module; you send this object back untouched when importing.
Meta 4
totalintHow many accounts were found.
methodstringThe listing method the module used.
pageintThe page you are on. Present on the paginated method.
limitintThe page size.
Errors 5
not_found404No such server.
module_not_found422The server module was not found.
import_not_supported422The module cannot list accounts.
list_failed422The accounts could not be listed.
insufficient_scope403The key lacks the required scope.
Request
curl -G 'https://panel.example.com/api/v1/admin/products/servers/34/importable' \
  -H "Authorization: Bearer $API_KEY" \
  -d limit=200
const 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

post/api/v1/admin/products/servers/{id}/import
Products/ImportServerAccounts admin 201

Turns the server's accounts into WISECP services. Each row is tied to a client and a product.

Body 1
itemsobject[]requiredThe rows to import. Each carries the raw account record from the listing endpoint, a client id, a product id, a price id and a start date; the end date is optional.
Response fields data — 1
importedobject[]The services created. Each element carries a service id and a name.
Errors 5
not_found404No such server.
items_required422items was empty.
no_valid_items422No row had a complete mapping. Rows missing a client, product or price id are dropped without a word.
import_failed422The import was refused.
insufficient_scope403The key lacks the required scope.
Request
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

Secrets cannot be read back

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 connection test does not try a stored server

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 action never half-applies

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.

A row with a gap is dropped silently on import

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.

Was this helpful?

Thanks for your feedback!

Still Need Help?

Our support team is here around the clock for anything you can't find above.