Introduction
This documentation is for developers working for NBT Host partners: agencies and companies that host their customers with us under their own brand. With the API you connect your own application (billing, CRM, order form) to the panel: a customer who pays you gets their account, website and email automatically, without anyone logging in to the panel.
There is no separate “NBT” API: you work directly with the Enhance API, inside your partner organisation. Every path on this page is a real API path. The read examples (GET) were run on our server on 21 October 2026, on Enhance 12.25.13, and the responses show the shape we received (the values are examples). The write examples are verified on a test organisation before the programme launches.
- Base URL:
https://server.nbt-host.com/api. - Create the token in the panel: Settings → Access tokens → Create token. Header:
Authorization: Bearer <tokenId>_<key>. - Almost every path starts with
/orgs/{org_id}: your organisation ($NBT_ORG) for customers and plans, the customer's organisation ($CLIENT) for its websites. - First complete flow: new customer with a website and email in 5 calls.
Concepts
The Enhance license is included
The Enhance license is included in the price of the partner plan and is provided by NBT Host. You have no direct contract with Enhance and you do not buy a license: you work in NBT Host's Enhance panel (our cluster), under your own brand and on your own domain. The servers, panel updates, security and server backups stay with us.
With Partner 50 and Partner 100, the included VPS joins the NBT Host cluster: your customers' websites run on it, but you do not get root access, and servers from other providers cannot be added.
Partner plans
Each plan gives you a reseller (partner) organisation with the limits below. The limits are the total for all your customers together; you can read the exact values, with what you have already used, at any time with GET /orgs/{ORG}/subscriptions (Limits).
Partner 30
- Up to 50 websites
- Shared web hosting
- 8 GB RAM
- Panel with your brand and domain
- Enhance plan:
nbthost_partener_30
Partner 50
- Up to 100 websites
- VPS included, in the NBT Host cluster
- 12 GB RAM · 6 vCPU
- 200 GB SSD or 100 GB NVMe
- Panel with your brand and domain
- Enhance plan:
nbthost_partener_50
Partner 100
- Up to 250 websites
- VPS included, in the NBT Host cluster
- 24 GB RAM · 8 vCPU
- 300 GB SSD or 150 GB NVMe
- Panel with your brand and domain
- Enhance plan:
nbthost_partener_100
The objects you work with
- Your partner organisation org · uuid · $NBT_ORG
- Your subscription to the Partner plan subscription · int — the resources you share among your customers
- Your customer plans plan · int — you create them from your limits, at your own price
- Your API tokens access token
- A customer org · uuid · $CLIENT
- The customer's subscription to one of your plans subscription · int
- Its websites website · uuid
- Domains: primary, alias, subdomain domain · uuid → DNS zone, SSL certificate, mailboxes
- MySQL databases, backups, applications (WordPress)
- Members (logins with roles) — who can log in to the panel as this customer
| Term | In the API | Meaning |
|---|---|---|
| Organisation | org (uuid) | The container for everything: your organisation is a “reseller”, each customer is a “customer” organisation under it. |
| Customer | customer | An organisation you create with POST /orgs/{ORG}/customers. Its parentId is your organisation. |
| Plan | plan (int) | Resources (websites, storage, mailboxes, databases), features (allowances) and PHP versions. Your plans belong to your organisation. |
| Subscription | subscription (int) | The link between an organisation and a plan. Websites are created on a subscription and use its quota. Suspension is done on the subscription. |
| Website | website (uuid) | A hosted website, with its own Unix user, PHP, files, databases and email. It has a primary domain. |
| Domain | domain (uuid) | A name attached to a website: primary, alias or subdomain. The DNS zone, SSL and email are addressed by domain.id. |
| Login, member | login, member | A login is the person (email + password) in your “realm”; a member is that login inside an organisation, with roles (Owner, SuperAdmin …). |
| Realm | — | The login space of your panel. Your customers log in on your panel domain, not on server.nbt-host.com. |
Authentication
The token, step by step in the panel
- Log in to the panel with your partner account (
https://server.nbt-host.comor your panel domain). - Settings → Access tokens → Create token.
- Name it after the application (e.g. “billing”) and choose the role:
SuperAdminfor full automation;Sysadmin,SupportorBusinessfor narrower access. AnOwnertoken cannot be created. - Set an expiry date and the IP addresses of the server that calls the API.
- The panel shows the token only once, as
<tokenId>_<key>, together with your organisation id. Copy both into a password vault or into your application's environment variables.
The whole token goes after Bearer: Authorization: Bearer 098afde9-…_jyDlM…. The key alone, without <tokenId>_, is rejected with 403 unauthorized.
# your details (treat the token like a password: environment variable, never in code)
export NBT_API=https://server.nbt-host.com/api
export NBT_ORG=… # your partner organisation
export NBT_TOKEN=… # <tokenId>_<cheie>
curl -sS "$NBT_API/orgs/$NBT_ORG" -H "Authorization: Bearer $NBT_TOKEN"
# your subscription: used / total resources
curl -sS "$NBT_API/orgs/$NBT_ORG/subscriptions" -H "Authorization: Bearer $NBT_TOKEN"<?php
// nbt-api.php — PHP 7.4+, cURL extension; retries on 429 / 503
function nbt_api(string $method, string $path, ?array $body = null): array
{
$url = (getenv('NBT_API') ?: 'https://server.nbt-host.com/api') . $path;
for ($try = 1; ; $try++) {
$ch = curl_init($url);
$headers = ['Authorization: Bearer ' . getenv('NBT_TOKEN'), 'Accept: application/json'];
if ($body !== null) {
$headers[] = 'Content-Type: application/json';
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADER => true,
CURLOPT_TIMEOUT => 120,
]);
$raw = curl_exec($ch);
if ($raw === false) {
throw new RuntimeException('cURL: ' . curl_error($ch));
}
$code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$head = substr($raw, 0, curl_getinfo($ch, CURLINFO_HEADER_SIZE));
$text = substr($raw, strlen($head));
if (($code === 429 || $code === 503) && $try < 4) {
preg_match('/^retry-after:\s*(\d+)/mi', $head, $m);
sleep((int) ($m[1] ?? 2 ** $try));
continue;
}
$data = json_decode($text, true);
if ($code >= 400) {
$msg = is_array($data) ? trim(($data['code'] ?? '') . ' ' . ($data['message'] ?? $data['detail'] ?? '')) : $text;
throw new RuntimeException("HTTP $code: $msg", $code);
}
return is_array($data) ? $data : ['value' => $data ?? $text];
}
}
$org = getenv('NBT_ORG'); // your partner organisation
print_r(nbt_api('GET', "/orgs/$org"));
print_r(nbt_api('GET', "/orgs/$org/subscriptions")); // your subscription: used / total resources// nbt-api.mjs — Node.js 18+ (built-in fetch); retries on 429 / 503
const API = process.env.NBT_API ?? 'https://server.nbt-host.com/api';
export async function api(method, path, body) {
for (let attempt = 1; ; attempt++) {
const res = await fetch(API + path, {
method,
headers: {
Authorization: `Bearer ${process.env.NBT_TOKEN}`,
Accept: 'application/json',
...(body !== undefined && { 'Content-Type': 'application/json' }),
},
body: body !== undefined ? JSON.stringify(body) : undefined,
});
if ((res.status === 429 || res.status === 503) && attempt < 4) {
const wait = Number(res.headers.get('retry-after')) || 2 ** attempt;
await new Promise((r) => setTimeout(r, wait * 1000));
continue;
}
const text = await res.text();
let data;
try { data = text ? JSON.parse(text) : null; } catch { data = text; }
if (!res.ok) {
const msg = typeof data === 'object' && data ? `${data.code ?? ''} ${data.message ?? data.detail ?? ''}` : data;
throw Object.assign(new Error(`HTTP ${res.status}: ${String(msg).trim()}`), { status: res.status, body: data });
}
return data;
}
}
// node verifica.mjs — your partner organisation
// import { api } from './nbt-api.mjs';
const org = process.env.NBT_ORG;
console.log(await api('GET', `/orgs/${org}`));
console.log(await api('GET', `/orgs/${org}/subscriptions`)); // your subscription: used / total resources{"id":"5f0c…","parentId":"…","name":"…","status":"active","createdAt":"…",
"subscriptionsCount":1,"websitesCount":0,"locale":"…"}
{"items":[{"id":31,"planId":63,"planName":"nbthost_partener_30","subscriberId":"5f0c…","vendorId":"…",
"status":"active","planType":"shared","allowedPhpVersions":["php82","php83","php84"],"defaultPhpVersion":"php83",
"resources":[{"name":"customers","total":50,"usage":0},{"name":"websites","total":50,"usage":0}, …],
"allowances":[…],"selections":[],"friendlyName":"…"}],"total":1}Tokens through the API
/orgs/{ORG}/access_tokensThe organisation's tokens (a plain array, no total; keys are never shown)./orgs/{ORG}/access_tokensNew token: {"roles":["SuperAdmin"],"friendlyName":"…","tokenExpires":"2027-01-01T00:00:00Z","allowedIps":["203.0.113.10"],"ipRestricted":true} → {"id","firstFive","unencryptedToken"}./orgs/{ORG}/access_tokens/{token_id}Change the role, expiry, name or IP addresses. Always send roles too: without it the panel answers 500./orgs/{ORG}/access_tokens/{token_id}Revocation: the token stops working immediately.curl -sS -X POST "$NBT_API/orgs/$NBT_ORG/access_tokens" \
-H "Authorization: Bearer $NBT_TOKEN" -H 'Content-Type: application/json' \
-d '{"roles":["SuperAdmin"],"friendlyName":"billing","tokenExpires":"2027-01-01T00:00:00Z","allowedIps":["203.0.113.10"],"ipRestricted":true}'{"id":"098afde9-…","firstFive":"jyDlM","unencryptedToken":"…"}
# the token to use: <id>_<unencryptedToken>Rotation and revocation
- Create a new token (in the panel or with
POST …/access_tokens) with the same role. - Deploy it to your application and check one call (
GET /orgs/{ORG}→ 200). - Delete the old token:
DELETE /orgs/{ORG}/access_tokens/{token_id}or in the panel.
A leaked token (in a git repository, a log, an email) is revoked immediately and replaced. You can extend the expiry with PATCH without creating a new token.
Good practice
- One token per application and per environment (production, test), with a clear name: you can revoke one without stopping the others.
- IP restriction and an expiry date on every token.
- The smallest role that does the job:
Supportfor a dashboard that only reads. - Keep the token in environment variables or a vault, never in code, in a browser or in a mobile app: the token can see all your customers.
- Never log the
Authorizationheader, nor the response ofGET /v2/domains/{id}/ssl(it contains the certificate's private key).
Base URL and conventions
- Base URL:
https://server.nbt-host.com/api— every path below goes after it.GET /version(no token) returns the version as text:12.25.13. - Requests with a body send
Content-Type: application/json. Responses are JSON, with a few exceptions that return a string (SSO links,/version). - Ids: organisations, websites, domains, logins and members use uuids; plans, subscriptions and backups use integers.
- Create returns
{"id": …}(201). A few operations return an empty body (e.g. databases, restores, certificates). - Long-running operations (backups, restores, certificates, WordPress installs) run in the background: read their status with the matching
GETevery few seconds. - Deletes without
forceare “soft”: the organisation or website becomesdeletedand can still be recovered for a few days if you ask us.
Pagination
Large lists accept offset and limit and return {"items":[…],"total":N}. Without limit you get the whole list (verified: 70 websites in one request), so request pages for large lists. Sorting: sortBy + sortOrder=asc|desc; searching: search.
| List | Parameters |
|---|---|
GET /orgs/{ORG}/customers | offset, limit, sortBy=id|name|status|subscriptionsCount|websitesCount|createdAt, sortOrder, search, status=active|suspended|deleted, planId, recursive |
GET /orgs/{ORG}/websites | offset, limit, sortBy=domain|subscription|org|size|createdAt, sortOrder, search, recursion=directCustomers|infinite (customers' websites too), planId, subscriptionId, status, isSuspended, showAliases, showDeleted |
GET /orgs/{ORG}/plans | offset, limit, sortBy=name, sortOrder |
Some small lists return only {"items":[…]} without total (a website's domains, databases, MySQL users, applications, backups), and GET …/access_tokens returns a bare array.
# all customers, 100 per page
OFFSET=0; LIMIT=100
while :; do
PAGE=$(curl -sS "$NBT_API/orgs/$NBT_ORG/customers?offset=$OFFSET&limit=$LIMIT&sortBy=name&sortOrder=asc" \
-H "Authorization: Bearer $NBT_TOKEN")
echo "$PAGE" | jq -r '.items[] | [.id, .name, .status, .websitesCount] | @tsv'
OFFSET=$((OFFSET + LIMIT))
[ "$OFFSET" -ge "$(echo "$PAGE" | jq .total)" ] && break
done<?php
require __DIR__ . '/nbt-api.php';
$org = getenv('NBT_ORG');
// all customers, 100 per page
$offset = 0; $limit = 100;
do {
$page = nbt_api('GET', "/orgs/$org/customers?offset=$offset&limit=$limit&sortBy=name&sortOrder=asc");
foreach ($page['items'] as $c) {
printf("%s\t%s\t%s\t%d\n", $c['id'], $c['name'], $c['status'], $c['websitesCount']);
}
$offset += $limit;
} while ($offset < $page['total']);import { api } from './nbt-api.mjs';
const org = process.env.NBT_ORG;
// all customers, 100 per page
async function* customers(limit = 100) {
for (let offset = 0; ; offset += limit) {
const page = await api('GET', `/orgs/${org}/customers?offset=${offset}&limit=${limit}&sortBy=name&sortOrder=asc`);
yield* page.items;
if (offset + limit >= page.total) return;
}
}
for await (const c of customers()) console.log(c.id, c.name, c.status, c.websitesCount);{"items":[{"id":"fea953a8-…","parentId":"5f0c…","name":"Client Company Ltd","status":"active",
"createdAt":"2026-10-21T09:14:03Z","subscriptionsCount":1,"websitesCount":1,"locale":"…"}, …],
"total":27}New customer with a website and email, in 5 calls
The flow most partners automate. You need a customer plan (Customer plans): PLAN_ID is its id from GET /orgs/$NBT_ORG/plans. The examples use jq with curl, and the helper functions from Authentication for PHP and Node.js.
# 1. the customer (a new organisation under yours) — keep the ids: repeating a POST creates another customer
CLIENT=$(curl -sS -X POST "$NBT_API/orgs/$NBT_ORG/customers" \
-H "Authorization: Bearer $NBT_TOKEN" -H 'Content-Type: application/json' \
-d '{"name":"Client Company Ltd"}' | jq -r .id)
# 2. its subscription to one of your plans (PLAN_ID: GET /orgs/$NBT_ORG/plans)
SUB=$(curl -sS -X POST "$NBT_API/orgs/$NBT_ORG/customers/$CLIENT/subscriptions" \
-H "Authorization: Bearer $NBT_TOKEN" -H 'Content-Type: application/json' \
-d "{\"planId\":$PLAN_ID}" | jq -r .id)
# 3. the website, with its domain and PHP version (one the plan allows)
SITE=$(curl -sS -X POST "$NBT_API/orgs/$CLIENT/websites" \
-H "Authorization: Bearer $NBT_TOKEN" -H 'Content-Type: application/json' \
-d "{\"domain\":\"client-company.com\",\"subscriptionId\":$SUB,\"phpVersion\":\"php83\"}" | jq -r .id)
# 4. the id of the primary domain (needed for email, DNS, SSL)
DOMAIN_ID=$(curl -sS "$NBT_API/orgs/$CLIENT/websites/$SITE" \
-H "Authorization: Bearer $NBT_TOKEN" | jq -r .domain.id)
# 5. the mailbox (password: 10+ characters, upper and lower case, digits, symbols; quota in bytes)
curl -sS -X POST "$NBT_API/orgs/$CLIENT/websites/$SITE/domains/$DOMAIN_ID/emails" \
-H "Authorization: Bearer $NBT_TOKEN" -H 'Content-Type: application/json' \
-d "{\"username\":\"contact\",\"mailboxPassword\":\"$MAIL_PASS\",\"quota\":1073741824}"<?php
require __DIR__ . '/nbt-api.php';
$org = getenv('NBT_ORG');
$planId = (int) getenv('PLAN_ID');
// 1. the customer (a new organisation under yours) — keep the ids: repeating a POST creates another customer
$client = nbt_api('POST', "/orgs/$org/customers", ['name' => 'Client Company Ltd'])['id'];
// 2. its subscription to one of your plans (PLAN_ID: GET /orgs/$NBT_ORG/plans)
$sub = nbt_api('POST', "/orgs/$org/customers/$client/subscriptions", ['planId' => $planId])['id'];
// 3. the website, with its domain and PHP version (one the plan allows)
$site = nbt_api('POST', "/orgs/$client/websites", [
'domain' => 'client-company.com', 'subscriptionId' => $sub, 'phpVersion' => 'php83',
])['id'];
// 4. the id of the primary domain (needed for email, DNS, SSL)
$domainId = nbt_api('GET', "/orgs/$client/websites/$site")['domain']['id'];
// 5. the mailbox (password: 10+ characters, upper and lower case, digits, symbols; quota in bytes)
nbt_api('POST', "/orgs/$client/websites/$site/domains/$domainId/emails", [
'username' => 'contact', 'mailboxPassword' => getenv('MAIL_PASS'), 'quota' => 1073741824,
]);
echo "client=$client subscription=$sub website=$site domain=$domainId\n";import { api } from './nbt-api.mjs';
const org = process.env.NBT_ORG;
const planId = Number(process.env.PLAN_ID);
// 1. the customer (a new organisation under yours) — keep the ids: repeating a POST creates another customer
const { id: client } = await api('POST', `/orgs/${org}/customers`, { name: 'Client Company Ltd' });
// 2. its subscription to one of your plans (PLAN_ID: GET /orgs/$NBT_ORG/plans)
const { id: sub } = await api('POST', `/orgs/${org}/customers/${client}/subscriptions`, { planId });
// 3. the website, with its domain and PHP version (one the plan allows)
const { id: site } = await api('POST', `/orgs/${client}/websites`, {
domain: 'client-company.com', subscriptionId: sub, phpVersion: 'php83',
});
// 4. the id of the primary domain (needed for email, DNS, SSL)
const { domain } = await api('GET', `/orgs/${client}/websites/${site}`);
// 5. the mailbox (password: 10+ characters, upper and lower case, digits, symbols; quota in bytes)
await api('POST', `/orgs/${client}/websites/${site}/domains/${domain.id}/emails`, {
username: 'contact', mailboxPassword: process.env.MAIL_PASS, quota: 1073741824,
});
console.log({ client, sub, site, domainId: domain.id });{"id":"fea953a8-…"} ← 1. POST /customers (uuid)
{"id":31} ← 2. POST /subscriptions (int)
{"id":"3d064861-…"} ← 3. POST /websites (uuid)
{"id":"3d064861-…","domain":{"id":"9b1e…","domain":"client-company.com","kind":"primary",…},"status":"active",
"phpVersion":"php83","subscriptionId":31,…} ← 4. GET /websites/{id}
{"id":"…"} ← 5. POST /emailsThe website gets its DNS zone on our name servers (or yours, if you set them), the chosen PHP version and your plan's memory and CPU limits. The Let's Encrypt certificate is issued automatically once the domain points to the server (SSL). To let the customer into the panel: Customer access.
The calls are not idempotent: a repeated POST /customers creates another customer, which takes a slot from your quota. Save every id immediately; when you retry, look first (GET /orgs/$NBT_ORG/customers?search=…, GET /orgs/$CLIENT/websites) and continue from the missing step.
Customers
/orgs/{ORG}/customersNew customer: {"name":"Client Company Ltd"} → {"id":"uuid"}. Takes a slot of the customers quota from creation./orgs/{CLIENT}The customer: name, status, parentId, subscriptionsCount, websitesCount./orgs/{CLIENT}Rename: {"name":"New Name Ltd"}./orgs/{CLIENT}/subscriptionsThe customer's subscriptions (with resource usage)./orgs/{CLIENT}/subscriptions/{subscription_id}Suspend / reactivate: {"isSuspended":true|false}./orgs/{CLIENT}“Soft” delete of the customer, with its websites.curl -sS "$NBT_API/orgs/$CLIENT" -H "Authorization: Bearer $NBT_TOKEN"{"id":"fea953a8-…","parentId":"5f0c…","name":"Client Company Ltd","status":"active",
"createdAt":"2026-10-21T09:14:03Z","subscriptionsCount":1,"websitesCount":1,"locale":"en"}Suspension and reactivation
You suspend the customer's subscription. While it is suspended, the customer's websites answer 404 and its email stops working; the data stays. A customer you suspended stays suspended even after we reactivate your plan (for example after a late payment).
# the customer's subscription
SUB=$(curl -sS "$NBT_API/orgs/$CLIENT/subscriptions" -H "Authorization: Bearer $NBT_TOKEN" | jq -r '.items[0].id')
# suspend (the customer's websites answer 404) / reactivate
curl -sS -X PATCH "$NBT_API/orgs/$CLIENT/subscriptions/$SUB" \
-H "Authorization: Bearer $NBT_TOKEN" -H 'Content-Type: application/json' -d '{"isSuspended":true}'
curl -sS -X PATCH "$NBT_API/orgs/$CLIENT/subscriptions/$SUB" \
-H "Authorization: Bearer $NBT_TOKEN" -H 'Content-Type: application/json' -d '{"isSuspended":false}' <?php
require __DIR__ . '/nbt-api.php';
$client = getenv('CLIENT');
// the customer's subscription
$sub = nbt_api('GET', "/orgs/$client/subscriptions")['items'][0]['id'];
// suspend (the customer's websites answer 404) / reactivate
nbt_api('PATCH', "/orgs/$client/subscriptions/$sub", ['isSuspended' => true]);
nbt_api('PATCH', "/orgs/$client/subscriptions/$sub", ['isSuspended' => false]);import { api } from './nbt-api.mjs';
const client = process.env.CLIENT;
// the customer's subscription
const { items: [sub] } = await api('GET', `/orgs/${client}/subscriptions`);
// suspend (the customer's websites answer 404) / reactivate
await api('PATCH', `/orgs/${client}/subscriptions/${sub.id}`, { isSuspended: true });
await api('PATCH', `/orgs/${client}/subscriptions/${sub.id}`, { isSuspended: false });Deleting
The order: any backups you no longer want, the websites, the subscription, then the organisation. Permanent deletion (?force=true) is reserved for the platform administrator: your token gets 403 only_mo_allowed. We do it on request; until then a “soft”-deleted customer can still be recovered.
curl -sS "$NBT_API/orgs/$CLIENT/websites/$SITE/backups" -H "Authorization: Bearer $NBT_TOKEN" # which backups exist
curl -sS -X DELETE "$NBT_API/orgs/$CLIENT/websites/$SITE" -H "Authorization: Bearer $NBT_TOKEN"
curl -sS -X DELETE "$NBT_API/orgs/$CLIENT/subscriptions/$SUB" -H "Authorization: Bearer $NBT_TOKEN"
curl -sS -X DELETE "$NBT_API/orgs/$CLIENT" -H "Authorization: Bearer $NBT_TOKEN"Customer plans
You decide what each customer gets: you build plans from your own limits and sell them at your own price. A plan cannot ask for more than you have (403 resource_out_of_quota), nor for a feature your plan does not include (403 non_existent_allowance).
/orgs/{ORG}/plansYour plans (paginated, sortBy=name)./orgs/{ORG}/plans/{plan_id}One plan, with resources, allowances, allowedPhpVersions./orgs/{ORG}/plansNew plan → {"id":17}./orgs/{ORG}/plans/{plan_id}The plan name: {"name":"…"}./orgs/{ORG}/plans/{plan_id}/resources/{name}One limit: {"name":"websites","total":3}./orgs/{ORG}/plans/{plan_id}/resourcesA resource the plan did not have yet./orgs/{ORG}/plans/{plan_id}/allowancesAdd features: [{"name":"featureCronjobs"}]./orgs/{ORG}/plans/{plan_id}/allowances/{name}Remove a feature./orgs/{ORG}/plans/{plan_id}Delete, only when it has no subscriptions left. A subscription you deleted (without force) still holds it: 403 unmigrated_subscriptions. Move the customers to another plan first, or ask us to delete them permanently.curl -sS -X POST "$NBT_API/orgs/$NBT_ORG/plans" \
-H "Authorization: Bearer $NBT_TOKEN" -H 'Content-Type: application/json' \
-d '{"name":"Business website",
"resources":[{"name":"websites","total":1},{"name":"diskspace","total":5368709120},
{"name":"mailboxes","total":5},{"name":"mysqlDbs","total":2},
{"name":"subdomains","total":5},{"name":"domainAliases","total":2},{"name":"forwarders"},{"name":"transfer"}],
"allowances":[{"name":"featureLetsEncrypt"},{"name":"backupsAllowAutomatic"},{"name":"featureDNSEditor"},{"name":"wptoolkit"}],
"selections":[],
"allowedPhpVersions":["php83","php84"],"defaultPhpVersion":"php83"}'{"id":17}- Resources (
name):websites,diskspace(bytes),mailboxes,mysqlDbs,subdomains,domainAliases,ftpUsers,forwarders,transfer,pageViews. A resource withouttotalis unlimited, but only if your plan has it unlimited too. A missing resource means 0. - Features (
allowances), those your own plan has:featureLetsEncrypt,featureSelfInstallSSL,featureDNSEditor,featureDNSAutoConfigure,featureCronjobs,featurePhpEditor,featureWebsiteClone,featureWebsiteCaching,featureCPanelMigrate,backupsAllowAutomatic,backupsAllowManual,backupsAllowSelfRestore,applicationsSoftwareInstaller,wptoolkitandwptoolkit…(managers, Shield, login obfuscation). allowedPhpVersionsis required and non-empty, fromphp82,php83,php84. Without it:400 invalid_argument“Allowed PHP versions cannot be empty since the parent package has restrictions”.mysqlDbs: at least one free database if the customer installs WordPress (the install creates its own).- Limits change one at a time, with
PUT …/resources/{name}. APATCHon the plan only changes its name. - Features only restrict what the customer can do in their panel; your partner token can make backups or install applications even on plans without that feature.
Subscriptions
/orgs/{ORG}/subscriptionsYour partner subscription: quotas and usage (resources[].usage / total)./orgs/{ORG}/customers/{CLIENT}/subscriptionsSubscribe the customer to a plan: {"planId":17} → {"id":31}./orgs/{ORG}/customers/{CLIENT}/subscriptionsThe customer's subscriptions to your plans./orgs/{CLIENT}/subscriptions/{subscription_id}One subscription, with its resources./orgs/{CLIENT}/subscriptions/{subscription_id}{"isSuspended":true}, {"planId":18} (change plan) or {"friendlyName":"…"}./orgs/{CLIENT}/subscriptions/{subscription_id}/calculate-resource-usageRecalculate usage (e.g. disk space)./orgs/{CLIENT}/subscriptions/{subscription_id}/bandwidthTraffic for the current month./orgs/{CLIENT}/subscriptions/{subscription_id}“Soft” delete of the subscription and the websites on it.curl -sS "$NBT_API/orgs/$CLIENT/subscriptions" -H "Authorization: Bearer $NBT_TOKEN"{"items":[{"id":31,"planId":17,"planName":"Business website","subscriberId":"fea953a8-…","vendorId":"5f0c…",
"status":"active","planType":"shared","friendlyName":"…","allowedPhpVersions":["php83","php84"],
"defaultPhpVersion":"php83","redisAllowed":false,"persistentAppsAllowed":false,"allowedApps":null,
"resources":[{"name":"websites","total":1,"usage":1},{"name":"diskspace","total":5368709120,"usage":81920}, …],
"allowances":[{"name":"featureLetsEncrypt"}, …],"selections":[]}],"total":1}diskspace is in bytes. A resource without total is unlimited.
Websites and domains
/orgs/{CLIENT}/websitesThe customer's websites (paginated). From your organisation with ?recursion=infinite: every website of every customer, with orgId./orgs/{CLIENT}/websitesNew website: {"domain":"client-company.com","subscriptionId":31,"phpVersion":"php83"} → {"id":"uuid"}./orgs/{CLIENT}/websites/{website_id}The website: domain, aliases, subdomains, status, phpVersion, size, unixUser, server IPs./orgs/{CLIENT}/websites/{website_id}{"phpVersion":"php84"}, {"isSuspended":true} (this website only)./orgs/{CLIENT}/websites/{website_id}“Soft” delete of the website./orgs/{CLIENT}/websites/{website_id}/domainsThe website's domains: domainId, domain, mappingKind, documentRoot./orgs/{CLIENT}/websites/{website_id}/domainsAlias or subdomain: {"domain":"…","kind":"alias"|"subdomain","documentRoot":"…"} → {"id"}./orgs/{CLIENT}/websites/{website_id}/domains/primaryMake another domain the primary one: {"domainId":"…"}./orgs/{CLIENT}/websites/{website_id}/domains/{domain_id}Change documentRoot./orgs/{CLIENT}/websites/{website_id}/domains/{domain_id}Remove an alias / subdomain (with its zone and mailboxes)./orgs/{CLIENT}/websites/{website_id}/cgroup_limitsThe website's memory and CPU limits (read only)./v2/websites/{website_id}/restart_phpRestart the website's PHP./orgs/{ORG}/domains/checkCheck whether a domain is already on the server: {"domain":"…"}.curl -sS "$NBT_API/orgs/$CLIENT/websites?limit=20&offset=0&sortBy=domain" -H "Authorization: Bearer $NBT_TOKEN"{"items":[{"id":"3d064861-…","domain":{"id":"9b1e…","domain":"client-company.com","documentRoot":"public_html",
"kind":"primary","cloudflareStatus":"…"},"aliases":[],"subdomains":[],"subscriptionId":31,"planId":17,
"plan":"Business website","status":"active","size":81920,"orgId":"fea953a8-…","kind":"normal",
"createdAt":"…","phpVersion":"php83","appServerName":"…","emailServerName":"…", …}],"total":1}curl -sS -X POST "$NBT_API/orgs/$CLIENT/websites/$SITE/domains" \
-H "Authorization: Bearer $NBT_TOKEN" -H 'Content-Type: application/json' \
-d '{"domain":"blog.client-company.com","kind":"subdomain","documentRoot":"public_html/blog"}'phpVersion: one of the versions the customer's plan allows (otherwise400 invalid_argument). Without it the website gets the plan'sdefaultPhpVersion.- Aliases and subdomains count against
domainAliasesandsubdomains, not as websites. Addon domains are not included. - A domain that is already on the server in another organisation, or a subdomain of it (for example
*.nbt-host.ro), is refused:403“Unable to create website outside of the org”.
DNS
Every domain has its own zone on our name servers (ns1.nbt-host.ro / ns2.nbt-host.ro, or yours, see Branding). The zone is addressed through the website and the domain_id.
/orgs/{CLIENT}/websites/{website_id}/domains/{domain_id}/dns-zoneThe zone: origin, soa, records[] (id, kind, name, value, proxy)./orgs/{CLIENT}/websites/{website_id}/domains/{domain_id}/dns-zone/recordsNew record → {"id"}./orgs/{CLIENT}/websites/{website_id}/domains/{domain_id}/dns-zone/records/{record_id}Change value, ttl, name./orgs/{CLIENT}/websites/{website_id}/domains/{domain_id}/dns-zone/records/{record_id}Delete the record./orgs/{CLIENT}/websites/{website_id}/domains/{domain_id}/dns-zoneThe SOA values./orgs/{CLIENT}/websites/{website_id}/domains/{domain_id}/dns-statusWhether the domain points to us./orgs/{CLIENT}/domainsThe organisation's domains, with dkim, spf, localRemote.curl -sS "$NBT_API/orgs/$CLIENT/websites/$SITE/domains/$DOMAIN_ID/dns-zone" -H "Authorization: Bearer $NBT_TOKEN"{"origin":"client-company.com.","soa":{"nameServer":"ns1.nbt-host.ro.","adminEmail":"…","refresh":10800,
"retry":3600,"expire":604800,"ttl":3600},
"records":[{"id":"7a1c…","kind":"A","name":"@","value":"185.230.138.193","proxy":false},
{"id":"…","kind":"CNAME","name":"www","value":"client-company.com.","proxy":false}, …]}curl -sS -X POST "$NBT_API/orgs/$CLIENT/websites/$SITE/domains/$DOMAIN_ID/dns-zone/records" \
-H "Authorization: Bearer $NBT_TOKEN" -H 'Content-Type: application/json' \
-d '{"kind":"TXT","name":"@","value":"google-site-verification=…","ttl":3600}'Record types (kind): A, AAAA, CNAME, MX, TXT, SRV, CAA, NS, SPF, PTR, DS. name is relative to the zone (@ for the apex, www, *.dev for a wildcard).
SSL (Let's Encrypt)
The Let's Encrypt certificate is issued and renewed automatically for every domain of a website once the domain points to the server. Request it manually only after a DNS change, or if you want it right away.
/v2/domains/{domain_id}/letsencrypt_preflightCheck whether it can be issued: {"canIssue":true} or {"canIssue":false,"error":"…"}./v2/domains/{domain_id}/letsencryptIssue (and install) the certificate; takes a few dozen seconds./v2/domains/{domain_id}/letsencrypt_mailThe certificate for mail.<domain>./v2/domains/{domain_id}/sslThe installed certificate: cn, issuer, issued, expires, sans, forceHttps (and the private key — never log it)./v2/domains/{domain_id}/ssl/force_sslHTTP → HTTPS redirect: body true or false./v2/domains/{domain_id}/sslYour own certificate (cert, key, chain) instead of Let's Encrypt.curl -sS -X POST "$NBT_API/v2/domains/$DOMAIN_ID/letsencrypt_preflight" -H "Authorization: Bearer $NBT_TOKEN"
curl -sS -X POST "$NBT_API/v2/domains/$DOMAIN_ID/letsencrypt" -H "Authorization: Bearer $NBT_TOKEN"
curl -sS -X PUT "$NBT_API/v2/domains/$DOMAIN_ID/ssl/force_ssl" \
-H "Authorization: Bearer $NBT_TOKEN" -H 'Content-Type: application/json' -d 'true'
# the status, without the private key:
curl -sS "$NBT_API/v2/domains/$DOMAIN_ID/ssl" -H "Authorization: Bearer $NBT_TOKEN" | jq '{cn, issuer, expires, sans, forceHttps}'{"cn":"client-company.com","issuer":"…Let's Encrypt…","expires":"2027-01-19T08:12:44Z",
"sans":["client-company.com","www.client-company.com"],"forceHttps":true}Mailboxes live on a website's domain. You create them on a domain_id; every other operation uses the full address (contact@client-company.com, URL-encoded as contact%40client-company.com).
/orgs/{CLIENT}/websites/{website_id}/emailsThe website's addresses: address, hasMailbox, quota {total, usage}, forwardersCount, aliases./orgs/{CLIENT}/websites/{website_id}/domains/{domain_id}/emailsMailbox: {"username":"contact","mailboxPassword":"…","quota":1073741824}. Forwarder only: {"username":"sales","forwarders":["jane@gmail.com"]}./orgs/{CLIENT}/websites/{website_id}/emails/{email_address}One address, with forwarders, aliases, blacklist, whitelist./orgs/{CLIENT}/websites/{website_id}/emails/{email_address}Password (mailboxPassword), quota, forwarders, aliases, isCatchAll, hasMailbox./orgs/{CLIENT}/websites/{website_id}/emails/{email_address}Delete the address (and the mailbox's messages)./orgs/{CLIENT}/websites/{website_id}/emails/{email_address}/autoresponderThe autoresponder (null if none)./orgs/{CLIENT}/websites/{website_id}/emails/{email_address}/autoresponderTurn the autoresponder on: {"enabled":true,"subject":"…","body":"…","startDate":"2026-12-20T00:00:00Z","endDate":"2027-01-05T00:00:00Z"}./orgs/{CLIENT}/websites/{website_id}/emails/{email_address}/autoresponderTurn it off./orgs/{CLIENT}/websites/{website_id}/emails/{email_address}/ssoDirect login to webmail (Roundcube), without a password: the answer is a redirect (307) to webmail./orgs/{CLIENT}/websites/{website_id}/emails/{email_address}/client-confIMAP / SMTP settings for Outlook, Thunderbird, phones./websites/{website_id}/domains/{domain_name}/email-authDKIM: {"dkim":true,"dkimPublicKey":"…"}./websites/{website_id}/domains/{domain_name}/email-authTurn DKIM on / off: {"dkim":true}./websites/{website_id}/domains/{domain_name}/email-auth/validateCheck DKIM and SPF in DNS./orgs/{CLIENT}/websites/{website_id}/domains/{domain_id}/local_remote{"localRemote":"local"}: the domain's email is hosted with us./orgs/{CLIENT}/websites/{website_id}/domains/{domain_id}/local_remote{"localRemote":"remote"} when the email is with another provider (Google Workspace, Microsoft 365): the server stops delivering the domain's messages locally.ADDR=contact%40client-company.com
curl -sS -X PATCH "$NBT_API/orgs/$CLIENT/websites/$SITE/emails/$ADDR" \
-H "Authorization: Bearer $NBT_TOKEN" -H 'Content-Type: application/json' \
-d '{"mailboxPassword":"'"$NEW_PASSWORD"'","forwarders":["jane.doe@gmail.com"]}'curl -sS "$NBT_API/orgs/$CLIENT/websites/$SITE/emails/$ADDR" -H "Authorization: Bearer $NBT_TOKEN"{"websiteId":"3d064861-…","address":"contact@client-company.com","mailboxName":"…","aliases":[],
"forwarders":[],"status":"active","quota":{"total":1073741824,"usage":4524},"spamDelivery":null,
"blacklist":[],"whitelist":[],"autorespondersCount":0,"hasMailbox":true,"isCatchAll":false,"ssoAvailable":true}- Passwords (mailboxes and logins): at least 10 characters, with upper and lower case letters, digits and symbols. A simple password:
400 invalid_argument“Password is not complex enough”. quotais in bytes (1073741824 = 1 GiB). Every mailbox counts against the plan'smailboxes.- DKIM is off by default on new domains: turn it on with
PUT …/email-authafter creating the mailbox, then check with…/email-auth/validate. If DNS is hosted elsewhere, add the TXT record fromdkimPublicKeythere. - The server has an hourly sending limit (against spam); bulk mail (newsletters) should go through a dedicated service.
MySQL databases
/orgs/{CLIENT}/websites/{website_id}/mysql-dbsThe website's databases./orgs/{CLIENT}/websites/{website_id}/mysql-dbsNew database: {"name":"shop"}. The panel adds the website prefix itself (e.g. client_1_shop): read the final name with GET./orgs/{CLIENT}/websites/{website_id}/mysql-dbs/{db_name}Delete the database./orgs/{CLIENT}/websites/{website_id}/mysql-usersThe users, with grants and accessHosts./orgs/{CLIENT}/websites/{website_id}/mysql-usersNew user: {"username":"app","password":"…"}./orgs/{CLIENT}/websites/{website_id}/mysql-users/{username}/privilegesRights on one database: {"dbName":"client_1_shop","grants":["all"]} ([] removes them)./orgs/{CLIENT}/websites/{website_id}/mysql-users/{username}The user's password./orgs/{CLIENT}/websites/{website_id}/mysql-users/{username}/access-hostsHosts the user may connect from./orgs/{CLIENT}/websites/{website_id}/mysql-users/{username}Delete the user./orgs/{CLIENT}/websites/{website_id}/mysql-dbs/{db_name}/ssoDirect login to phpMyAdmin on that database. Today it answers 404 not_found “phpMyAdmin domain not configured”: open phpMyAdmin from the panel.curl -sS "$NBT_API/orgs/$CLIENT/websites/$SITE/mysql-users" -H "Authorization: Bearer $NBT_TOKEN"{"items":[{"username":"client_1_app","accessHosts":["localhost","…"],"authPlugin":"…",
"grants":{"client_1_shop":["all"]},"createdAt":"…","isEphemeral":false}]}PostgreSQL is not included in the partner plans. Databases count against mysqlDbs.
Backups and restore
Websites have automatic (daily) backups, made by us on the backup server. Through the API you list them, make an on-demand backup and restore files, databases and mailboxes.
/orgs/{CLIENT}/websites/{website_id}/backupsThe backups: id, startedAt, finishedAt, kind (automatic / manual), size, mysqlDbsCount, emailsCount, statuses./orgs/{CLIENT}/websites/{website_id}/backups?includeEmails=trueOn-demand backup (body {} or {"description":"before update"})./orgs/{CLIENT}/websites/{website_id}/backups/{backup_id}One backup (404 not_found “Restore point … not found” if it does not exist)./orgs/{CLIENT}/websites/{website_id}/status/backupThe backup in progress, if any (null otherwise)./orgs/{CLIENT}/websites/{website_id}/backups/{backup_id}Restore, with the options in the body./orgs/{CLIENT}/websites/{website_id}/backups/{backup_id}/restore_statusRestore status./orgs/{CLIENT}/websites/{website_id}/backups/{backup_id}/directory_treeThe files in the backup (for a partial restore)./orgs/{CLIENT}/websites/{website_id}/backups/{backup_id}Delete a backup. Today it answers 404 not_found “remote_storage” on our platform: backups are removed automatically after the retention period; if one must be deleted, write to us.curl -sS -X POST "$NBT_API/orgs/$CLIENT/websites/$SITE/backups?includeEmails=true" \
-H "Authorization: Bearer $NBT_TOKEN" -H 'Content-Type: application/json' -d '{"description":"before update"}'
curl -sS -X PUT "$NBT_API/orgs/$CLIENT/websites/$SITE/backups/$BACKUP_ID" \
-H "Authorization: Bearer $NBT_TOKEN" -H 'Content-Type: application/json' \
-d '{"restoreFiles":true,"restoreAllEmails":true,"restoreDatabases":["client_1_shop"]}'- The restore body:
restoreFiles(the website's home directory),restoreOnlyFiles(selected paths only),restoreDatabases(missing = every database in the backup;[]= none),restoreEmailsorrestoreAllEmails. Always send it explicitly. - A restore brings back the state of an existing website; it does not recreate a website, database or mailbox deleted through the API. For a deleted website, contact us.
- A backup uses space on the backup server, not your plan's storage.
WordPress
/orgs/{CLIENT}/websites/{website_id}/appsInstall: {"app":"wordpress","path":"","adminUsername":"…","adminPassword":"…","adminEmail":"…"} → {"id"}./orgs/{CLIENT}/websites/{website_id}/appsThe website's applications: id, app, version, path./orgs/{CLIENT}/websites/{website_id}/apps/{app_id}/wordpress/infoversion, site_url, plugin_count, user_count, has_woocommerce./orgs/{CLIENT}/websites/{website_id}/apps/{app_id}/wordpress/versionThe version, read live./orgs/{CLIENT}/websites/{website_id}/apps/{app_id}/wordpress/versionUpdate WordPress to the latest version (no body; if it is already up to date, 200 and nothing to do)./orgs/{CLIENT}/websites/{website_id}/apps/{app_id}/wordpress/usersWordPress users: id, login, email./orgs/{CLIENT}/websites/{website_id}/apps/{app_id}/wordpress/users/{user_id}/ssoDirect login link to wp-admin./orgs/{CLIENT}/websites/{website_id}/apps/{app_id}/wordpress/pluginsThe plugins./orgs/{CLIENT}/websites/{website_id}/apps/{app_id}/wordpress/pluginsInstall a plugin./orgs/{CLIENT}/websites/{website_id}/apps/{app_id}/wordpress/plugins/{plugin}/versionUpdate a plugin./orgs/{CLIENT}/websites/{website_id}/apps/{app_id}/wordpress/themesThemes: name, status, version, update, autoUpdate./v2/apps/{app_id}/wordpress/maintenance-modeMaintenance mode./orgs/{CLIENT}/websites/{website_id}/apps/{app_id}Uninstall (a website backup is made first).curl -sS -X POST "$NBT_API/orgs/$CLIENT/websites/$SITE/apps" \
-H "Authorization: Bearer $NBT_TOKEN" -H 'Content-Type: application/json' \
-d '{"app":"wordpress","path":"","adminUsername":"client-admin","adminPassword":"'"$WP_PASS"'","adminEmail":"jane@client-company.com"}'
curl -sS "$NBT_API/orgs/$CLIENT/websites/$SITE/apps/$APP/wordpress/info" -H "Authorization: Bearer $NBT_TOKEN"{"id":"c0ffee12-…"}
{"version":"6.8.3","site_url":"https://client-company.com","plugin_count":3,"user_count":1,"has_woocommerce":false}The install enables PHP on the website and creates its own database (the customer's plan needs a free mysqlDbs slot). It takes about a minute; until then …/wordpress/info may return an error.
Customer access to the panel and SSO
First put the panel on your own domain (Branding). Your customers live in your realm and log in on your panel domain (for example https://panel.your-agency.com), not on server.nbt-host.com.
Option 1: you create the account (no email)
# the customer's login, in your realm (orgId = the CUSTOMER organisation)
LOGIN=$(curl -sS -X POST "$NBT_API/logins?orgId=$CLIENT" \
-H "Authorization: Bearer $NBT_TOKEN" -H 'Content-Type: application/json' \
-d "{\"email\":\"jane@client-company.com\",\"name\":\"Jane Doe\",\"password\":\"$LOGIN_PASS\"}" | jq -r .id)
# member with the Owner role in the customer organisation
MEMBER=$(curl -sS -X POST "$NBT_API/orgs/$CLIENT/members" \
-H "Authorization: Bearer $NBT_TOKEN" -H 'Content-Type: application/json' \
-d "{\"loginId\":\"$LOGIN\",\"roles\":[\"Owner\"]}" | jq -r .id)
# one-time, short-lived login link on your panel domain
curl -sS "$NBT_API/orgs/$CLIENT/members/$MEMBER/sso" -H "Authorization: Bearer $NBT_TOKEN"<?php
require __DIR__ . '/nbt-api.php';
$client = getenv('CLIENT');
// the customer's login, in your realm (orgId = the CUSTOMER organisation)
$login = nbt_api('POST', "/logins?orgId=$client", [
'email' => 'jane@client-company.com', 'name' => 'Jane Doe', 'password' => getenv('LOGIN_PASS'),
])['id'];
// member with the Owner role in the customer organisation
$member = nbt_api('POST', "/orgs/$client/members", ['loginId' => $login, 'roles' => ['Owner']])['id'];
// one-time, short-lived login link on your panel domain
$link = nbt_api('GET', "/orgs/$client/members/$member/sso")['value'];
header('Location: ' . $link);import { api } from './nbt-api.mjs';
const client = process.env.CLIENT;
// the customer's login, in your realm (orgId = the CUSTOMER organisation)
const { id: loginId } = await api('POST', `/logins?orgId=${client}`, {
email: 'jane@client-company.com', name: 'Jane Doe', password: process.env.LOGIN_PASS,
});
// member with the Owner role in the customer organisation
const { id: member } = await api('POST', `/orgs/${client}/members`, { loginId, roles: ['Owner'] });
// one-time, short-lived login link on your panel domain
const link = await api('GET', `/orgs/${client}/members/${member}/sso`);
console.log(link);{"id":"a5a86df8-…"} ← POST /logins
{"id":"0c7d…"} ← POST /members
"https://panel.your-agency.com/api/login/sessions/sso?otp=…" ← GET /ssoOption 2: an email invitation
curl -sS -X POST "$NBT_API/orgs/$CLIENT/invites" \
-H "Authorization: Bearer $NBT_TOKEN" -H 'Content-Type: application/json' \
-d '{"email":"jane@client-company.com","name":"Jane Doe","roles":["SuperAdmin"]}'The panel sends the email from the address set in Settings → Branding, with the link on your panel domain; the customer chooses a password. With "sendEmail":false no email is sent and the link comes back in the response (201, a string), for you to send. The Owner role cannot be given by invitation (400): create the owner with option 1.
/orgs/{CLIENT}/membersThe organisation's members (and pending invitations)./logins?orgId={CLIENT}New login in your realm: {"email","name","password"} → {"id"}./orgs/{CLIENT}/membersMember: {"loginId":"…","roles":["Owner"]}./orgs/{CLIENT}/members/{member_id}The member's roles./orgs/{CLIENT}/members/{member_id}Remove access./v2/logins/{login_id}/passwordNew password for a login: {"newPassword":"…"}./orgs/{CLIENT}/members/{member_id}/ssoOne-time, short-lived login link (SSO) for a “Go to panel” button in your application./v2/orgs/{ORG}/customers/loginsYour customers' logins./orgs/{CLIENT}/invitesEmail invitation (see above).Roles: Owner (everything in the customer organisation), SuperAdmin, Sysadmin, Support, Business, SiteAccess (selected websites only). The SSO link works once and expires quickly: request it right when the customer clicks the button.
Branding, panel domain, name servers
Your panel carries your brand and your domain: customers see your logo, colours, name and support address, not NBT Host. Everything can be done in the panel (Settings → Branding, Settings → Platform) or through the API.
/orgs/{ORG}/websites?kind=controlPanelYour panel domain: {"domain":"panel.your-agency.com"}. Before that, in your DNS: panel.your-agency.com A 185.230.138.193. If you already have one: 409 already_exists — add the new name as an alias on the panel website (POST …/websites/{id}/domains with "kind":"alias") and make it primary (PUT …/domains/primary)./orgs/{ORG}/websites?kind=controlPanelThe panel website, with domain.id (for the certificate)./orgs/{ORG}/name-serversYour name servers: {"domain":"ns1.your-agency.com"}, then the same for ns2./orgs/{ORG}/branding/settingsThe branding settings./orgs/{ORG}/branding/settings/{name}One setting (e.g. sge_from_email = the sender address of the panel's emails, as a JSON string)./orgs/{ORG}/branding/logoLogo (multipart, field logo; max. 200 KiB, png / svg / jpg). Likewise: inverse-logo, favicon, login-image (max. 2 MiB), inverse-icon./orgs/{ORG}/branding/logoRemove the logo (same for the other images).curl -sS -X POST "$NBT_API/orgs/$NBT_ORG/websites?kind=controlPanel" \
-H "Authorization: Bearer $NBT_TOKEN" -H 'Content-Type: application/json' -d '{"domain":"panel.your-agency.com"}'
curl -sS -X PUT "$NBT_API/orgs/$NBT_ORG/branding/logo" -H "Authorization: Bearer $NBT_TOKEN" -F 'logo=@logo.svg'- The panel domain's certificate is installed automatically within minutes if DNS already points to the server; otherwise
letsencrypt_preflight+letsencrypton itsdomain.id(SSL). - Your name servers: in your domain's zone add
ns1.your-agency.com A 185.230.138.193andns2.your-agency.com A 213.199.48.100. From then on your customers' zones carry your NS records, and customers set them at their registrar. - Put the sender address (
sge_from_email) on a domain hosted with us, so the panel's emails (password resets, invitations) pass SPF / DKIM.
Errors
An error has an HTTP status and usually a JSON body: {"code":"…","detail":"…","message":"…"} (detail and message may be missing). Code against code, not against the text of message.
| HTTP | code | When it happens (real responses) |
|---|---|---|
| 400 | invalid_argument | A wrong field: password too simple, PHP version not allowed, plan without allowedPhpVersions, token with the Owner role, malformed token (“invalid length: expected length 32…”). |
| 400 | — (text) | Wrong query parameter, plain-text body: Query deserialize error: invalid digit found in string (e.g. limit=abc). |
| 401 | no_session_token | The Authorization header is missing. |
| 403 | unauthorized | Wrong, expired or revoked token, or a token used from another IP; or a resource outside your organisation (“Only a reseller or the MO may perform this operation”, “Unable to create website outside of the org”). |
| 403 | only_mo_allowed | An operation reserved for the platform administrator: force=true, servers, server groups, memory / CPU limits. |
| 403 | resource_out_of_quota | Over quota: a second website on a one-website plan, a 6th mailbox out of 5, a customer plan bigger than your plan (“The total is 10, the usage is 0, while the requested quota is: 11”). |
| 403 | non_existent_allowance | A feature your plan does not include (e.g. featureSSH). |
| 403 | suspended | Your plan is suspended: “Cannot perform actions on suspended entity 'subscription'”. Reads work, writes on customers do not. |
| 403 | not_allowed | A feature not included in the plan (e.g. “Branding not enabled on your subscription”). |
| 404 | not_found | detail says what is missing: {"code":"not_found","detail":"website"}, "subscription", "backup", "org". |
| 404 | — (text) | An id in the path that is not a valid uuid: UUID parsing failed: invalid character…. |
| 429 | — | Too many requests (see Limits). Wait Retry-After seconds, or 2, 4, 8 s, and retry. |
| 500 | — (text) | “An unknown error occurred, please try again later”: a server error or an error in an application on the website (e.g. the plugin list of a broken WordPress). Retry once; if it persists, contact us with the time and the path. |
Limits
- Your plan's quotas (customers, websites, storage, mailboxes, databases, subdomains, aliases, FTP) are the total for all customers:
GET /orgs/{ORG}/subscriptions→resources[]withusageandtotal(nototal= unlimited). A customer plan cannot exceed them, and the N+1th website gets403 resource_out_of_quotaeven if the customer's plan still has room. - Usage is counted on use: a customer takes a slot from the moment its organisation is created (not when it is subscribed), a website from its creation. An empty, forgotten organisation still holds a slot: delete it.
- Memory and CPU: every website gets your plan's limits (
GET …/cgroup_limits); they cannot be changed through the API. - Requests: we have not hit a limit (90 requests in 17 seconds, all 200), but that is not a guarantee: stay below 5 requests per second, paginate, and handle
429with retries (the helpers in Authentication do). - Outgoing email has a server-wide hourly limit, against spam.
What you cannot do
- Root or SSH — neither you nor your customers. SSH keys and crontabs added beyond the plan are removed automatically by our hourly check.
- External servers — you cannot add servers, choose server groups or change memory and CPU limits (
403 only_mo_allowed). The Partner 50 / 100 VPS belongs to the NBT Host cluster. - Sub-resellers — your customer plans cannot have customers: “Cannot create a plan with customers where sub-reselling it not enabled” (
400). - Overselling — the total sold to customers cannot exceed your quotas.
- Permanent deletion (
force=true) — we do it on request. - Addon domains and PostgreSQL — not included.
- Domains of other organisations and their subdomains (for example
*.nbt-host.ro) — refused with403. - Customers, servers and backups of NBT Host or other partners — your token sees only your organisation and your customers (
403 unauthorized).
Frequently asked questions
Do I need to buy an Enhance license or have a contract with Enhance?
No. The Enhance license is included in the price of the partner plan and is provided by NBT Host. You have no direct contract with Enhance: you work in NBT Host's Enhance panel, under your own brand and on your own domain.
Do I get root on the Partner 50 / 100 VPS? Can I add my own server?
No. The included VPS joins the NBT Host cluster: your customers' websites run on it, while updates, security and backups stay with us. Servers from other providers cannot be added.
Which API URL do I use if my panel is on my own domain?
https://server.nbt-host.com/api. Your panel domain is for people (you and your customers, in a browser); integrations use the address above.
I lost my token. Can I see it again?
No: the panel shows it only once. Create a new one and delete the old one (Rotation).
Do my customers see NBT Host anywhere?
Not in the panel, once you have set your domain, logo, favicon and sender address. DNS zones can carry your name servers instead of ours.
What happens to my customers if my subscription is not paid?
When your plan is suspended, your customers are suspended too (their websites answer 404). Your token can still read, but writes get 403 suspended. On reactivation, everything that was suspended with your plan comes back.
Is there a test environment (sandbox)?
Not a separate one. Create a test plan with small limits and a test customer, then delete them. A test customer takes a quota slot while it exists.
Support and contact
For the API, the panel and the servers, contact us. For your customers (their websites, email and complaints) you are the first point of contact: you support them, we support you.
To help us help you quickly, send: the method and path (without the token), the time of the request, the HTTP status and the response body. Never send us your token; if it ended up in a message, revoke it.
