Parteneri NBT · Documentație API ROENID Suport API

Parteneri NBT · API v12

Documentația API pentru parteneri

Tot ce face panoul tău de partener se face și din cod: clienți, pachete, site-uri, DNS, SSL, email, baze de date, copii de siguranță și WordPress. API-ul este cel al panoului Enhance din clusterul NBT Host, cu tokenul organizației tale.

Cuprins

Introducere

Documentația e pentru dezvoltatorii partenerilor NBT Host: agenții și firme care își găzduiesc clienții la noi, sub marca lor. Cu API-ul îți legi propria aplicație (facturare, CRM, formularul de comandă) de panou: un client care plătește la tine primește automat contul, site-ul și emailul, fără să intri în panou.

Nu există un API separat „NBT”: lucrezi direct cu API-ul Enhance, în organizația ta de partener. Tot ce apare aici are calea reală a API-ului. Exemplele de citire (GET) au fost rulate pe serverul nostru pe 21.10.2026, cu Enhance 12.25.13, iar răspunsurile arată forma primită atunci (valorile sunt de exemplu). Exemplele de scriere sunt verificate pe o organizație de test înainte de lansarea programului.

Pe scurt
  • URL-ul de bază: https://server.nbt-host.com/api.
  • Tokenul îl faci în panou: Setări → Tokenuri de acces → Creați jeton. Antetul: Authorization: Bearer <tokenId>_<cheie>.
  • Aproape toate căile încep cu /orgs/{org_id}: organizația ta ($NBT_ORG) pentru clienți și pachete, organizația clientului ($CLIENT) pentru site-urile lui.
  • Primul flux complet: client nou cu site și email în 5 apeluri.

Concepte

Licența Enhance e inclusă

Licența Enhance este inclusă în prețul pachetului de partener și o asigură NBT Host. Nu ai contract direct cu Enhance și nu cumperi licență: lucrezi în panoul Enhance al NBT Host (clusterul nostru), sub marca și pe domeniul tău. Serverele, actualizările panoului, securitatea și copiile de siguranță ale serverelor rămân la noi.

La Partener 50 și Partener 100, serverul VPS inclus intră în clusterul NBT Host: site-urile clienților tăi rulează pe el, dar nu primești acces root, iar servere de la alți furnizori nu se pot adăuga.

Pachetele de partener

Fiecare pachet îți dă o organizație de revânzător (partener) cu limitele de mai jos. Limitele sunt totalul pentru toți clienții tăi la un loc; valorile exacte, cu ce ai folosit deja, le citești oricând cu GET /orgs/{ORG}/subscriptions (Limite).

Partener 30

30 € / lună
  • Până la 50 site-uri
  • Găzduire web partajată
  • 8 GB RAM
  • Panou cu marca și domeniul tău
  • Pachetul în Enhance: nbthost_partener_30

Partener 50

50 € / lună
  • Până la 100 site-uri
  • VPS inclus, în clusterul NBT Host
  • 12 GB RAM · 6 vCPU
  • 200 GB SSD sau 100 GB NVMe
  • Panou cu marca și domeniul tău
  • Pachetul în Enhance: nbthost_partener_50

Partener 100

100 € / lună
  • Până la 250 site-uri
  • VPS inclus, în clusterul NBT Host
  • 24 GB RAM · 8 vCPU
  • 300 GB SSD sau 150 GB NVMe
  • Panou cu marca și domeniul tău
  • Pachetul în Enhance: nbthost_partener_100

Obiectele cu care lucrezi

  • Organizația ta de partener org · uuid · $NBT_ORG
    • Abonamentul tău la pachetul Partener subscription · int — resursele pe care le împarți clienților
    • Pachetele tale pentru clienți plan · int — le faci tu, din limitele tale, cu prețul tău
    • Tokenurile tale API access token
    • Un client org · uuid · $CLIENT
      • Abonamentul clientului la unul din pachetele tale subscription · int
      • Site-urile lui website · uuid
        • Domenii: principal, alias, subdomeniu domain · uuid → zona DNS, certificatul SSL, căsuțele de email
        • Baze de date MySQL, copii de siguranță, aplicații (WordPress)
      • Membrii (loginuri cu roluri) — cine intră în panou ca acest client
TermenÎn APICe înseamnă
Organizațieorg (uuid)Containerul pentru tot: organizația ta e un „reseller”, fiecare client e o organizație „customer” sub ea.
ClientcustomerO organizație creată de tine cu POST /orgs/{ORG}/customers. Are parentId = organizația ta.
Pachetplan (int)Resurse (site-uri, spațiu, căsuțe, baze de date), funcții (allowances) și versiuni PHP. Pachetele tale sunt ale organizației tale.
Abonamentsubscription (int)Legătura dintre o organizație și un pachet. Site-urile se creează pe un abonament și consumă din el. Suspendarea se face pe abonament.
Sitewebsite (uuid)Un site găzduit, cu utilizatorul lui Unix, PHP, fișiere, baze de date și email. Are un domeniu principal.
Domeniudomain (uuid)Un nume legat de site: primary, alias sau subdomain. Zona DNS, SSL-ul și emailul se adresează prin domain.id.
Login, membrulogin, memberLoginul e persoana (email + parolă) în „realm”-ul tău; membrul e loginul într-o organizație, cu roluri (Owner, SuperAdmin …).
Realm—Spațiul de loginuri al panoului tău. Clienții tăi intră pe domeniul panoului tău, nu pe server.nbt-host.com.

Autentificarea

Tokenul, pas cu pas în panou

  1. Intră în panou cu contul tău de partener (https://server.nbt-host.com sau domeniul panoului tău).
  2. Setări → Tokenuri de acces → Creați jeton (în engleză: Settings → Access tokens → Create token).
  3. Dă-i un nume după aplicație (de ex. „facturare”), alege rolul: SuperAdmin pentru automatizarea completă; Sysadmin, Support sau Business pentru acces mai restrâns. Un token Owner nu se poate face.
  4. Pune o dată de expirare și IP-urile serverului de pe care chemi API-ul.
  5. Panoul îți arată tokenul o singură dată, sub forma <tokenId>_<cheie>, împreună cu id-ul organizației tale. Copiază-le într-un seif de parole sau în variabilele de mediu ale aplicației.

Tokenul întreg se pune după Bearer: Authorization: Bearer 098afde9-…_jyDlM…. Doar cheia, fără <tokenId>_, e respinsă cu 403 unauthorized.

Exemplu
curl
# datele tale (tokenul: ca o parolă — din variabilă de mediu, nu din cod)
export NBT_API=https://server.nbt-host.com/api
export NBT_ORG=…        # organizația ta de partener
export NBT_TOKEN=…      # <tokenId>_<cheie>

curl -sS "$NBT_API/orgs/$NBT_ORG" -H "Authorization: Bearer $NBT_TOKEN"
# abonamentul tău: resursele folosite / totale
curl -sS "$NBT_API/orgs/$NBT_ORG/subscriptions" -H "Authorization: Bearer $NBT_TOKEN"
PHP
<?php
// nbt-api.php — PHP 7.4+, extensia cURL; reîncearcă la 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');                       // organizația ta de partener
print_r(nbt_api('GET', "/orgs/$org"));
print_r(nbt_api('GET', "/orgs/$org/subscriptions"));   // abonamentul tău: resursele folosite / totale
Node.js
// nbt-api.mjs — Node.js 18+ (fetch inclus); reîncearcă la 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 — organizația ta de partener
// 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`)); // abonamentul tău: resursele folosite / totale
Răspuns
{"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}

Tokenuri prin API

GET/orgs/{ORG}/access_tokensTokenurile organizației (listă simplă, fără total; cheile nu apar).
POST/orgs/{ORG}/access_tokensToken nou: {"roles":["SuperAdmin"],"friendlyName":"…","tokenExpires":"2027-01-01T00:00:00Z","allowedIps":["203.0.113.10"],"ipRestricted":true} → {"id","firstFive","unencryptedToken"}.
PATCH/orgs/{ORG}/access_tokens/{token_id}Schimbi rolul, expirarea, numele sau IP-urile. Trimite mereu și roles: fără el, panoul răspunde 500.
DELETE/orgs/{ORG}/access_tokens/{token_id}Revocarea: tokenul nu mai merge din clipa aceea.
Token nou, prin API
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":"facturare","tokenExpires":"2027-01-01T00:00:00Z","allowedIps":["203.0.113.10"],"ipRestricted":true}'
Răspuns
{"id":"098afde9-…","firstFive":"jyDlM","unencryptedToken":"…"}
# tokenul de folosit: <id>_<unencryptedToken>

Rotirea și revocarea

  1. Faci un token nou (din panou sau cu POST …/access_tokens), cu același rol.
  2. Îl pui în aplicație și verifici un apel (GET /orgs/{ORG} → 200).
  3. Ștergi tokenul vechi: DELETE /orgs/{ORG}/access_tokens/{token_id} sau din panou.

Un token scăpat (într-un depozit git, un log, un email) se revocă imediat, apoi se face altul. Expirarea o poți prelungi cu PATCH, fără token nou.

Bune practici

  • Un token pe aplicație și pe mediu (producție, test), cu nume clar: îl revoci pe unul fără să le oprești pe celelalte.
  • Restricție pe IP și dată de expirare la fiecare token.
  • Rolul cel mai mic care îți ajunge: Support pentru un tablou de bord care doar citește.
  • Tokenul stă în variabile de mediu sau într-un seif, niciodată în cod, în browser sau în aplicații mobile: tokenul vede toți clienții tăi.
  • Nu scrie în loguri antetul Authorization și nici răspunsul de la GET /v2/domains/{id}/ssl (conține cheia privată a certificatului).

URL-ul de bază și convențiile

  • URL-ul de bază: https://server.nbt-host.com/api — toate căile de mai jos se pun după el. GET /version (fără token) întoarce versiunea, ca text: 12.25.13.
  • Cererile cu corp trimit Content-Type: application/json. Răspunsurile sunt JSON, cu câteva excepții care întorc un șir (legăturile SSO, /version).
  • Id-urile: organizațiile, site-urile, domeniile, loginurile și membrii au uuid; pachetele, abonamentele și copiile de siguranță au numere întregi.
  • Crearea întoarce {"id": …} (201). Câteva operații întorc corp gol (de ex. baza de date, restaurarea, certificatul).
  • Operații care durează (copia de siguranță, restaurarea, certificatul, instalarea WordPress) pornesc în fundal: citești starea cu GET-ul corespunzător, din câteva în câteva secunde.
  • Ștergerile fără force sunt „soft”: organizația sau site-ul trece pe deleted și se mai poate recupera câteva zile, la cererea ta către noi.

Paginarea

Listele mari primesc offset și limit și întorc {"items":[…],"total":N}. Fără limit, primești toată lista (verificat: 70 de site-uri dintr-o cerere), așa că pentru liste mari cere pagini. Sortarea: sortBy + sortOrder=asc|desc; căutarea: search.

ListaParametri
GET /orgs/{ORG}/customersoffset, limit, sortBy=id|name|status|subscriptionsCount|websitesCount|createdAt, sortOrder, search, status=active|suspended|deleted, planId, recursive
GET /orgs/{ORG}/websitesoffset, limit, sortBy=domain|subscription|org|size|createdAt, sortOrder, search, recursion=directCustomers|infinite (și site-urile clienților), planId, subscriptionId, status, isSuspended, showAliases, showDeleted
GET /orgs/{ORG}/plansoffset, limit, sortBy=name, sortOrder

Unele liste mici întorc doar {"items":[…]}, fără total (domeniile unui site, bazele de date, utilizatorii MySQL, aplicațiile, copiile de siguranță), iar GET …/access_tokens întoarce direct un tablou.

Exemplu
curl
# toți clienții, câte 100 pe pagină
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
<?php
require __DIR__ . '/nbt-api.php';
$org = getenv('NBT_ORG');

// toți clienții, câte 100 pe pagină
$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']);
Node.js
import { api } from './nbt-api.mjs';
const org = process.env.NBT_ORG;

// toți clienții, câte 100 pe pagină
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);
Răspuns
{"items":[{"id":"fea953a8-…","parentId":"5f0c…","name":"Firma Clientului SRL","status":"active",
  "createdAt":"2026-10-21T09:14:03Z","subscriptionsCount":1,"websitesCount":1,"locale":"…"}, …],
 "total":27}

Client nou cu site și email, în 5 apeluri

Fluxul pe care îl automatizează majoritatea partenerilor. Ai nevoie de un pachet pentru clienți (Pachete pentru clienți): PLAN_ID e id-ul lui din GET /orgs/$NBT_ORG/plans. Exemplele folosesc jq pentru curl și funcțiile ajutătoare din Autentificare pentru PHP și Node.js.

Exemplu
curl
# 1. clientul (o organizație nouă, sub a ta) — păstrează id-urile: un POST repetat face încă un client
CLIENT=$(curl -sS -X POST "$NBT_API/orgs/$NBT_ORG/customers" \
  -H "Authorization: Bearer $NBT_TOKEN" -H 'Content-Type: application/json' \
  -d '{"name":"Firma Clientului SRL"}' | jq -r .id)

# 2. abonamentul lui la unul din pachetele tale (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. site-ul, cu domeniul și versiunea PHP (una permisă de pachet)
SITE=$(curl -sS -X POST "$NBT_API/orgs/$CLIENT/websites" \
  -H "Authorization: Bearer $NBT_TOKEN" -H 'Content-Type: application/json' \
  -d "{\"domain\":\"firma-clientului.ro\",\"subscriptionId\":$SUB,\"phpVersion\":\"php83\"}" | jq -r .id)

# 4. id-ul domeniului principal (pentru email, DNS, SSL)
DOMAIN_ID=$(curl -sS "$NBT_API/orgs/$CLIENT/websites/$SITE" \
  -H "Authorization: Bearer $NBT_TOKEN" | jq -r .domain.id)

# 5. căsuța de email (parola: min. 10 caractere, litere mari și mici, cifre, simboluri; quota în octeți)
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
<?php
require __DIR__ . '/nbt-api.php';
$org    = getenv('NBT_ORG');
$planId = (int) getenv('PLAN_ID');

// 1. clientul (o organizație nouă, sub a ta) — păstrează id-urile: un POST repetat face încă un client
$client = nbt_api('POST', "/orgs/$org/customers", ['name' => 'Firma Clientului SRL'])['id'];

// 2. abonamentul lui la unul din pachetele tale (PLAN_ID: GET /orgs/$NBT_ORG/plans)
$sub = nbt_api('POST', "/orgs/$org/customers/$client/subscriptions", ['planId' => $planId])['id'];

// 3. site-ul, cu domeniul și versiunea PHP (una permisă de pachet)
$site = nbt_api('POST', "/orgs/$client/websites", [
    'domain' => 'firma-clientului.ro', 'subscriptionId' => $sub, 'phpVersion' => 'php83',
])['id'];

// 4. id-ul domeniului principal (pentru email, DNS, SSL)
$domainId = nbt_api('GET', "/orgs/$client/websites/$site")['domain']['id'];

// 5. căsuța de email (parola: min. 10 caractere, litere mari și mici, cifre, simboluri; quota în octeți)
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";
Node.js
import { api } from './nbt-api.mjs';
const org = process.env.NBT_ORG;
const planId = Number(process.env.PLAN_ID);

// 1. clientul (o organizație nouă, sub a ta) — păstrează id-urile: un POST repetat face încă un client
const { id: client } = await api('POST', `/orgs/${org}/customers`, { name: 'Firma Clientului SRL' });

// 2. abonamentul lui la unul din pachetele tale (PLAN_ID: GET /orgs/$NBT_ORG/plans)
const { id: sub } = await api('POST', `/orgs/${org}/customers/${client}/subscriptions`, { planId });

// 3. site-ul, cu domeniul și versiunea PHP (una permisă de pachet)
const { id: site } = await api('POST', `/orgs/${client}/websites`, {
  domain: 'firma-clientului.ro', subscriptionId: sub, phpVersion: 'php83',
});

// 4. id-ul domeniului principal (pentru email, DNS, SSL)
const { domain } = await api('GET', `/orgs/${client}/websites/${site}`);

// 5. căsuța de email (parola: min. 10 caractere, litere mari și mici, cifre, simboluri; quota în octeți)
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 });
Răspuns
{"id":"fea953a8-…"}        ← 1. POST /customers           (uuid)
{"id":31}                   ← 2. POST /subscriptions       (int)
{"id":"3d064861-…"}         ← 3. POST /websites            (uuid)
{"id":"3d064861-…","domain":{"id":"9b1e…","domain":"firma-clientului.ro","kind":"primary",…},"status":"active",
 "phpVersion":"php83","subscriptionId":31,…}               ← 4. GET /websites/{id}
{"id":"…"}                  ← 5. POST /emails
Ce urmează singur

Site-ul primește zona DNS pe nameserverele noastre (sau pe ale tale, dacă le-ai setat), PHP-ul ales și limitele de memorie și procesor ale pachetului tău. Certificatul Let's Encrypt se emite automat după ce domeniul arată spre server (SSL). Ca să intre clientul în panou: Accesul clientului.

Dacă un pas eșuează

Apelurile nu sunt idempotente: un POST /customers repetat face încă un client, care ocupă un loc din cota ta. Salvează fiecare id imediat; la reluare, caută întâi (GET /orgs/$NBT_ORG/customers?search=…, GET /orgs/$CLIENT/websites) și continuă de la pasul care lipsește.

Clienții

GET/orgs/{ORG}/customersClienții tăi, paginat (parametri).
POST/orgs/{ORG}/customersClient nou: {"name":"Firma Clientului SRL"} → {"id":"uuid"}. Ocupă un loc din cota customers de la creare.
GET/orgs/{CLIENT}Datele clientului: name, status, parentId, subscriptionsCount, websitesCount.
PATCH/orgs/{CLIENT}Redenumirea: {"name":"Nume nou SRL"}.
GET/orgs/{CLIENT}/subscriptionsAbonamentele clientului (cu resursele folosite).
PATCH/orgs/{CLIENT}/subscriptions/{subscription_id}Suspendare / reactivare: {"isSuspended":true|false}.
DELETE/orgs/{CLIENT}Ștergerea „soft” a clientului, cu site-urile lui.
GET /orgs/{CLIENT}
curl -sS "$NBT_API/orgs/$CLIENT" -H "Authorization: Bearer $NBT_TOKEN"
Răspuns
{"id":"fea953a8-…","parentId":"5f0c…","name":"Firma Clientului SRL","status":"active",
 "createdAt":"2026-10-21T09:14:03Z","subscriptionsCount":1,"websitesCount":1,"locale":"ro"}

Suspendarea și reactivarea

Se suspendă abonamentul clientului. Cât e suspendat, site-urile lui răspund 404 și emailul lui nu mai merge; datele rămân. Un client suspendat de tine rămâne suspendat și după ce noi reactivăm pachetul tău (de exemplu după o plată întârziată).

Exemplu
curl
# abonamentul clientului
SUB=$(curl -sS "$NBT_API/orgs/$CLIENT/subscriptions" -H "Authorization: Bearer $NBT_TOKEN" | jq -r '.items[0].id')

# suspendă (site-urile clientului răspund 404) / reactivează
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
<?php
require __DIR__ . '/nbt-api.php';
$client = getenv('CLIENT');

// abonamentul clientului
$sub = nbt_api('GET', "/orgs/$client/subscriptions")['items'][0]['id'];

// suspendă (site-urile clientului răspund 404) / reactivează
nbt_api('PATCH', "/orgs/$client/subscriptions/$sub", ['isSuspended' => true]);
nbt_api('PATCH', "/orgs/$client/subscriptions/$sub", ['isSuspended' => false]);
Node.js
import { api } from './nbt-api.mjs';
const client = process.env.CLIENT;

// abonamentul clientului
const { items: [sub] } = await api('GET', `/orgs/${client}/subscriptions`);

// suspendă (site-urile clientului răspund 404) / reactivează
await api('PATCH', `/orgs/${client}/subscriptions/${sub.id}`, { isSuspended: true });
await api('PATCH', `/orgs/${client}/subscriptions/${sub.id}`, { isSuspended: false });

Ștergerea

Ordinea: copiile de siguranță pe care nu le mai vrei, site-urile, abonamentul, apoi organizația. Ștergerea definitivă (?force=true) e rezervată administratorului platformei: tokenul tău primește 403 only_mo_allowed. O facem noi, la cerere; până atunci clientul șters „soft” se mai poate recupera.

Ștergerea unui client
curl -sS "$NBT_API/orgs/$CLIENT/websites/$SITE/backups" -H "Authorization: Bearer $NBT_TOKEN"   # ce copii are
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"

Pachete pentru clienți

Tu hotărăști ce primește fiecare client: faci pachete din limitele tale și le vinzi cu prețul tău. Un pachet nu poate cere mai mult decât ai (403 resource_out_of_quota), nici o funcție pe care pachetul tău n-o are (403 non_existent_allowance).

GET/orgs/{ORG}/plansPachetele tale (paginat, sortBy=name).
GET/orgs/{ORG}/plans/{plan_id}Un pachet, cu resources, allowances, allowedPhpVersions.
POST/orgs/{ORG}/plansPachet nou → {"id":17}.
PATCH/orgs/{ORG}/plans/{plan_id}Numele pachetului: {"name":"…"}.
PUT/orgs/{ORG}/plans/{plan_id}/resources/{name}O limită: {"name":"websites","total":3}.
POST/orgs/{ORG}/plans/{plan_id}/resourcesO resursă care lipsea din pachet.
POST/orgs/{ORG}/plans/{plan_id}/allowancesFuncții adăugate: [{"name":"featureCronjobs"}].
DELETE/orgs/{ORG}/plans/{plan_id}/allowances/{name}O funcție scoasă.
DELETE/orgs/{ORG}/plans/{plan_id}Ștergerea, doar dacă nu mai are abonamente. Un abonament șters de tine (fără force) încă îl ține: 403 unmigrated_subscriptions. Mută clienții pe alt pachet înainte sau scrie-ne să ștergem definitiv.
POST /orgs/{ORG}/plans
curl -sS -X POST "$NBT_API/orgs/$NBT_ORG/plans" \
  -H "Authorization: Bearer $NBT_TOKEN" -H 'Content-Type: application/json' \
  -d '{"name":"Site de prezentare",
       "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"}'
Răspuns
{"id":17}
  • Resursele (name): websites, diskspace (octeți), mailboxes, mysqlDbs, subdomains, domainAliases, ftpUsers, forwarders, transfer, pageViews. Una fără total e nelimitată, dar doar dacă și pachetul tău o are nelimitată. Una care lipsește înseamnă 0.
  • Funcțiile (allowances), cele pe care le are și pachetul tău: featureLetsEncrypt, featureSelfInstallSSL, featureDNSEditor, featureDNSAutoConfigure, featureCronjobs, featurePhpEditor, featureWebsiteClone, featureWebsiteCaching, featureCPanelMigrate, backupsAllowAutomatic, backupsAllowManual, backupsAllowSelfRestore, applicationsSoftwareInstaller, wptoolkit și wptoolkit… (manageri, Shield, ascunderea loginului).
  • allowedPhpVersions e obligatoriu și nevid, din php82, php83, php84. Fără el: 400 invalid_argument „Allowed PHP versions cannot be empty since the parent package has restrictions”.
  • mysqlDbs: cel puțin o bază liberă dacă clientul instalează WordPress (instalarea își face baza ei).
  • Limitele se schimbă câte una, cu PUT …/resources/{name}. Un PATCH pe pachet schimbă doar numele.
  • Funcțiile restrâng doar ce face clientul în panoul lui; tokenul tău de partener poate face copii de siguranță sau instala aplicații și pe pachete fără funcția respectivă.

Abonamentele

GET/orgs/{ORG}/subscriptionsAbonamentul tău de partener: cotele și cât ai folosit (resources[].usage / total).
POST/orgs/{ORG}/customers/{CLIENT}/subscriptionsAbonează clientul la un pachet: {"planId":17} → {"id":31}.
GET/orgs/{ORG}/customers/{CLIENT}/subscriptionsAbonamentele clientului la pachetele tale.
GET/orgs/{CLIENT}/subscriptions/{subscription_id}Un abonament, cu resursele lui.
PATCH/orgs/{CLIENT}/subscriptions/{subscription_id}{"isSuspended":true}, {"planId":18} (schimbarea pachetului) sau {"friendlyName":"…"}.
PUT/orgs/{CLIENT}/subscriptions/{subscription_id}/calculate-resource-usageRecalculează consumul (de ex. spațiul pe disc).
GET/orgs/{CLIENT}/subscriptions/{subscription_id}/bandwidthTraficul din luna curentă.
DELETE/orgs/{CLIENT}/subscriptions/{subscription_id}Ștergerea „soft” a abonamentului și a site-urilor de pe el.
GET /orgs/{CLIENT}/subscriptions
curl -sS "$NBT_API/orgs/$CLIENT/subscriptions" -H "Authorization: Bearer $NBT_TOKEN"
Răspuns
{"items":[{"id":31,"planId":17,"planName":"Site de prezentare","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 e în octeți. O resursă fără total e nelimitată.

Site-uri și domenii

GET/orgs/{CLIENT}/websitesSite-urile clientului (paginat). Din organizația ta, cu ?recursion=infinite: toate site-urile tuturor clienților, cu orgId.
POST/orgs/{CLIENT}/websitesSite nou: {"domain":"firma-clientului.ro","subscriptionId":31,"phpVersion":"php83"} → {"id":"uuid"}.
GET/orgs/{CLIENT}/websites/{website_id}Site-ul: domain, aliases, subdomains, status, phpVersion, size, unixUser, IP-urile serverelor.
PATCH/orgs/{CLIENT}/websites/{website_id}{"phpVersion":"php84"}, {"isSuspended":true} (doar site-ul).
DELETE/orgs/{CLIENT}/websites/{website_id}Ștergerea „soft” a site-ului.
GET/orgs/{CLIENT}/websites/{website_id}/domainsDomeniile site-ului: domainId, domain, mappingKind, documentRoot.
POST/orgs/{CLIENT}/websites/{website_id}/domainsAlias sau subdomeniu: {"domain":"…","kind":"alias"|"subdomain","documentRoot":"…"} → {"id"}.
PUT/orgs/{CLIENT}/websites/{website_id}/domains/primaryAlt domeniu devine principal: {"domainId":"…"}.
PATCH/orgs/{CLIENT}/websites/{website_id}/domains/{domain_id}Schimbă documentRoot.
DELETE/orgs/{CLIENT}/websites/{website_id}/domains/{domain_id}Scoate un alias / subdomeniu (cu zona și căsuțele lui).
GET/orgs/{CLIENT}/websites/{website_id}/cgroup_limitsLimitele de memorie și procesor ale site-ului (doar citire).
POST/v2/websites/{website_id}/restart_phpRepornește PHP-ul site-ului.
POST/orgs/{ORG}/domains/checkVerifică dacă un domeniu e deja pe server: {"domain":"…"}.
GET /orgs/{CLIENT}/websites?limit=20&offset=0
curl -sS "$NBT_API/orgs/$CLIENT/websites?limit=20&offset=0&sortBy=domain" -H "Authorization: Bearer $NBT_TOKEN"
Răspuns
{"items":[{"id":"3d064861-…","domain":{"id":"9b1e…","domain":"firma-clientului.ro","documentRoot":"public_html",
   "kind":"primary","cloudflareStatus":"…"},"aliases":[],"subdomains":[],"subscriptionId":31,"planId":17,
   "plan":"Site de prezentare","status":"active","size":81920,"orgId":"fea953a8-…","kind":"normal",
   "createdAt":"…","phpVersion":"php83","appServerName":"…","emailServerName":"…", …}],"total":1}
Subdomeniu pe un site existent
curl -sS -X POST "$NBT_API/orgs/$CLIENT/websites/$SITE/domains" \
  -H "Authorization: Bearer $NBT_TOKEN" -H 'Content-Type: application/json' \
  -d '{"domain":"blog.firma-clientului.ro","kind":"subdomain","documentRoot":"public_html/blog"}'
  • phpVersion: una din versiunile permise de pachetul clientului (altfel 400 invalid_argument). Fără ea, site-ul ia defaultPhpVersion al pachetului.
  • Aliasurile și subdomeniile se numără la domainAliases și subdomains, nu ca site-uri. Domeniile addon nu sunt incluse.
  • Un domeniu care e deja pe server în altă organizație, sau un subdomeniu al lui (de exemplu *.nbt-host.ro), e refuzat: 403 „Unable to create website outside of the org”.

DNS

Fiecare domeniu are zona lui pe serverele noastre de nume (ns1.nbt-host.ro / ns2.nbt-host.ro, sau ale tale, vezi Branding). Zona se adresează prin site și domain_id.

GET/orgs/{CLIENT}/websites/{website_id}/domains/{domain_id}/dns-zoneZona: origin, soa, records[] (id, kind, name, value, proxy).
POST/orgs/{CLIENT}/websites/{website_id}/domains/{domain_id}/dns-zone/recordsÎnregistrare nouă → {"id"}.
PATCH/orgs/{CLIENT}/websites/{website_id}/domains/{domain_id}/dns-zone/records/{record_id}Schimbă value, ttl, name.
DELETE/orgs/{CLIENT}/websites/{website_id}/domains/{domain_id}/dns-zone/records/{record_id}Șterge înregistrarea.
PATCH/orgs/{CLIENT}/websites/{website_id}/domains/{domain_id}/dns-zoneValorile SOA.
GET/orgs/{CLIENT}/websites/{website_id}/domains/{domain_id}/dns-statusDacă domeniul arată spre noi.
GET/orgs/{CLIENT}/domainsDomeniile organizației, cu dkim, spf, localRemote.
Zona și o înregistrare nouă
curl -sS "$NBT_API/orgs/$CLIENT/websites/$SITE/domains/$DOMAIN_ID/dns-zone" -H "Authorization: Bearer $NBT_TOKEN"
Răspuns
{"origin":"firma-clientului.ro.","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":"firma-clientului.ro.","proxy":false}, …]}
POST …/dns-zone/records
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}'

Tipurile (kind): A, AAAA, CNAME, MX, TXT, SRV, CAA, NS, SPF, PTR, DS. name e relativ la zonă (@ pentru rădăcină, www, *.dev pentru wildcard).

SSL (Let's Encrypt)

Certificatul Let's Encrypt se emite și se reînnoiește singur pentru fiecare domeniu al unui site, după ce domeniul arată spre server. Îl ceri de mână doar după o mutare de DNS sau dacă vrei să-l ai imediat.

POST/v2/domains/{domain_id}/letsencrypt_preflightVerifică dacă se poate emite: {"canIssue":true} sau {"canIssue":false,"error":"…"}.
POST/v2/domains/{domain_id}/letsencryptEmite (și instalează) certificatul; durează câteva zeci de secunde.
POST/v2/domains/{domain_id}/letsencrypt_mailCertificatul pentru mail.<domeniu>.
GET/v2/domains/{domain_id}/sslCertificatul instalat: cn, issuer, issued, expires, sans, forceHttps (și cheia privată — nu-l scrie în loguri).
PUT/v2/domains/{domain_id}/ssl/force_sslRedirecționarea HTTP → HTTPS: corpul true sau false.
POST/v2/domains/{domain_id}/sslUn certificat propriu (cert, cheie, lanț), dacă nu vrei Let's Encrypt.
Certificatul, de mână
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'
# starea, fără cheia privată:
curl -sS "$NBT_API/v2/domains/$DOMAIN_ID/ssl" -H "Authorization: Bearer $NBT_TOKEN" | jq '{cn, issuer, expires, sans, forceHttps}'
Răspuns
{"cn":"firma-clientului.ro","issuer":"…Let's Encrypt…","expires":"2027-01-19T08:12:44Z",
 "sans":["firma-clientului.ro","www.firma-clientului.ro"],"forceHttps":true}

Email

Căsuțele stau pe domeniul unui site. Crearea se face pe domain_id, iar restul operațiilor pe adresa întreagă (contact@firma-clientului.ro, codificată în URL ca contact%40firma-clientului.ro).

GET/orgs/{CLIENT}/websites/{website_id}/emailsAdresele site-ului: address, hasMailbox, quota {total, usage}, forwardersCount, aliases.
POST/orgs/{CLIENT}/websites/{website_id}/domains/{domain_id}/emailsCăsuță: {"username":"contact","mailboxPassword":"…","quota":1073741824}. Doar redirecționare: {"username":"vanzari","forwarders":["ion@gmail.com"]}.
GET/orgs/{CLIENT}/websites/{website_id}/emails/{email_address}O adresă, cu forwarders, aliases, blacklist, whitelist.
PATCH/orgs/{CLIENT}/websites/{website_id}/emails/{email_address}Parola (mailboxPassword), quota, forwarders, aliases, isCatchAll, hasMailbox.
DELETE/orgs/{CLIENT}/websites/{website_id}/emails/{email_address}Șterge adresa (și mesajele căsuței).
GET/orgs/{CLIENT}/websites/{website_id}/emails/{email_address}/autoresponderRăspunsul automat (null dacă nu e).
POST/orgs/{CLIENT}/websites/{website_id}/emails/{email_address}/autoresponderPornește răspunsul automat: {"enabled":true,"subject":"…","body":"…","startDate":"2026-12-20T00:00:00Z","endDate":"2027-01-05T00:00:00Z"}.
DELETE/orgs/{CLIENT}/websites/{website_id}/emails/{email_address}/autoresponderÎl oprește.
GET/orgs/{CLIENT}/websites/{website_id}/emails/{email_address}/ssoIntrare directă în webmail (Roundcube), fără parolă: răspunsul e o redirecționare (307) spre webmail.
GET/orgs/{CLIENT}/websites/{website_id}/emails/{email_address}/client-confSetările IMAP / SMTP pentru Outlook, Thunderbird, telefon.
GET/websites/{website_id}/domains/{domain_name}/email-authDKIM: {"dkim":true,"dkimPublicKey":"…"}.
PUT/websites/{website_id}/domains/{domain_name}/email-authPornește / oprește DKIM: {"dkim":true}.
GET/websites/{website_id}/domains/{domain_name}/email-auth/validateVerifică DKIM și SPF în DNS.
GET/orgs/{CLIENT}/websites/{website_id}/domains/{domain_id}/local_remote{"localRemote":"local"}: emailul domeniului e la noi.
PUT/orgs/{CLIENT}/websites/{website_id}/domains/{domain_id}/local_remote{"localRemote":"remote"} când emailul e la alt furnizor (Google Workspace, Microsoft 365): serverul nu mai livrează local mesajele pentru domeniu.
Parola și redirecționările unei căsuțe
ADDR=contact%40firma-clientului.ro
curl -sS -X PATCH "$NBT_API/orgs/$CLIENT/websites/$SITE/emails/$ADDR" \
  -H "Authorization: Bearer $NBT_TOKEN" -H 'Content-Type: application/json' \
  -d '{"mailboxPassword":"'"$PAROLA_NOUA"'","forwarders":["ion.popescu@gmail.com"]}'
GET …/emails/{email_address}
curl -sS "$NBT_API/orgs/$CLIENT/websites/$SITE/emails/$ADDR" -H "Authorization: Bearer $NBT_TOKEN"
Răspuns
{"websiteId":"3d064861-…","address":"contact@firma-clientului.ro","mailboxName":"…","aliases":[],
 "forwarders":[],"status":"active","quota":{"total":1073741824,"usage":4524},"spamDelivery":null,
 "blacklist":[],"whitelist":[],"autorespondersCount":0,"hasMailbox":true,"isCatchAll":false,"ssoAvailable":true}
  • Parolele (căsuțe și loginuri): minimum 10 caractere, cu litere mari și mici, cifre și simboluri. O parolă simplă: 400 invalid_argument „Password is not complex enough”.
  • quota e în octeți (1073741824 = 1 GiB). Fiecare căsuță se numără la mailboxes din pachet.
  • DKIM e oprit implicit pe domeniile noi: pornește-l cu PUT …/email-auth după ce faci căsuța, apoi verifică cu …/email-auth/validate. Dacă DNS-ul e la alt furnizor, pune acolo înregistrarea TXT din dkimPublicKey.
  • Serverul are o limită de trimitere pe oră (împotriva spamului); trimiterile în masă (newslettere) se fac printr-un serviciu dedicat.

Baze de date MySQL

GET/orgs/{CLIENT}/websites/{website_id}/mysql-dbsBazele de date ale site-ului.
POST/orgs/{CLIENT}/websites/{website_id}/mysql-dbsBază nouă: {"name":"magazin"}. Panoul pune singur prefixul site-ului (de ex. firma_1_magazin): ia numele final din GET.
DELETE/orgs/{CLIENT}/websites/{website_id}/mysql-dbs/{db_name}Șterge baza.
GET/orgs/{CLIENT}/websites/{website_id}/mysql-usersUtilizatorii, cu grants și accessHosts.
POST/orgs/{CLIENT}/websites/{website_id}/mysql-usersUtilizator nou: {"username":"app","password":"…"}.
PUT/orgs/{CLIENT}/websites/{website_id}/mysql-users/{username}/privilegesDrepturile pe o bază: {"dbName":"firma_1_magazin","grants":["all"]} ([] le scoate).
PUT/orgs/{CLIENT}/websites/{website_id}/mysql-users/{username}Parola utilizatorului.
POST/orgs/{CLIENT}/websites/{website_id}/mysql-users/{username}/access-hostsGazde de pe care se poate conecta.
DELETE/orgs/{CLIENT}/websites/{website_id}/mysql-users/{username}Șterge utilizatorul.
GET/orgs/{CLIENT}/websites/{website_id}/mysql-dbs/{db_name}/ssoIntrare directă în phpMyAdmin pe baza respectivă. Azi răspunde 404 not_found „phpMyAdmin domain not configured”: intri în phpMyAdmin din panou.
GET …/mysql-users
curl -sS "$NBT_API/orgs/$CLIENT/websites/$SITE/mysql-users" -H "Authorization: Bearer $NBT_TOKEN"
Răspuns
{"items":[{"username":"firma_1_app","accessHosts":["localhost","…"],"authPlugin":"…",
  "grants":{"firma_1_magazin":["all"]},"createdAt":"…","isEphemeral":false}]}

PostgreSQL nu e inclus în pachetele de partener. Bazele se numără la mysqlDbs.

Copii de siguranță și restaurare

Site-urile au copii automate (zilnic), făcute de noi pe serverul de backup. Prin API le listezi, faci o copie la cerere și restaurezi fișierele, bazele de date și căsuțele.

GET/orgs/{CLIENT}/websites/{website_id}/backupsCopiile: id, startedAt, finishedAt, kind (automatic / manual), size, mysqlDbsCount, emailsCount, stările.
POST/orgs/{CLIENT}/websites/{website_id}/backups?includeEmails=trueCopie la cerere (corp {} sau {"description":"înainte de actualizare"}).
GET/orgs/{CLIENT}/websites/{website_id}/backups/{backup_id}O copie (404 not_found „Restore point … not found” dacă nu există).
GET/orgs/{CLIENT}/websites/{website_id}/status/backupCopia în curs, dacă e (null altfel).
PUT/orgs/{CLIENT}/websites/{website_id}/backups/{backup_id}Restaurarea, cu opțiunile în corp.
GET/orgs/{CLIENT}/websites/{website_id}/backups/{backup_id}/restore_statusStarea restaurării.
GET/orgs/{CLIENT}/websites/{website_id}/backups/{backup_id}/directory_treeFișierele din copie (pentru restaurarea parțială).
DELETE/orgs/{CLIENT}/websites/{website_id}/backups/{backup_id}Șterge o copie. Azi răspunde 404 not_found „remote_storage” pe platforma noastră: copiile se șterg singure, după perioada de păstrare; dacă trebuie ștearsă una anume, scrie-ne.
Copie, apoi restaurare
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":"inainte de actualizare"}'

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":["firma_1_magazin"]}'
  • Corpul restaurării: restoreFiles (directorul site-ului), restoreOnlyFiles (doar anumite căi), restoreDatabases (lipsă = toate bazele din copie; [] = niciuna), restoreEmails sau restoreAllEmails. Dă-l explicit.
  • Restaurarea readuce starea unui site existent; nu recreează un site, o bază sau o căsuță ștearsă prin API. Pentru un site șters, ne scrii.
  • O copie de siguranță ocupă loc pe serverul de backup, nu din spațiul pachetului.

WordPress

POST/orgs/{CLIENT}/websites/{website_id}/appsInstalează: {"app":"wordpress","path":"","adminUsername":"…","adminPassword":"…","adminEmail":"…"} → {"id"}.
GET/orgs/{CLIENT}/websites/{website_id}/appsAplicațiile site-ului: id, app, version, path.
GET/orgs/{CLIENT}/websites/{website_id}/apps/{app_id}/wordpress/infoversion, site_url, plugin_count, user_count, has_woocommerce.
GET/orgs/{CLIENT}/websites/{website_id}/apps/{app_id}/wordpress/versionVersiunea, citită în timp real.
PATCH/orgs/{CLIENT}/websites/{website_id}/apps/{app_id}/wordpress/versionActualizarea WordPress la ultima versiune (fără corp; dacă e deja la zi, 200 fără nimic de făcut).
GET/orgs/{CLIENT}/websites/{website_id}/apps/{app_id}/wordpress/usersUtilizatorii WordPress: id, login, email.
GET/orgs/{CLIENT}/websites/{website_id}/apps/{app_id}/wordpress/users/{user_id}/ssoLegătură de intrare directă în wp-admin.
GET/orgs/{CLIENT}/websites/{website_id}/apps/{app_id}/wordpress/pluginsModulele (pluginurile).
POST/orgs/{CLIENT}/websites/{website_id}/apps/{app_id}/wordpress/pluginsInstalează un modul.
PATCH/orgs/{CLIENT}/websites/{website_id}/apps/{app_id}/wordpress/plugins/{plugin}/versionActualizează un modul.
GET/orgs/{CLIENT}/websites/{website_id}/apps/{app_id}/wordpress/themesTemele: name, status, version, update, autoUpdate.
PUT/v2/apps/{app_id}/wordpress/maintenance-modeModul de mentenanță.
DELETE/orgs/{CLIENT}/websites/{website_id}/apps/{app_id}Dezinstalează (face întâi o copie de siguranță a site-ului).
Instalare WordPress, apoi starea
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":"admin-firma","adminPassword":"'"$WP_PASS"'","adminEmail":"ion@firma-clientului.ro"}'

curl -sS "$NBT_API/orgs/$CLIENT/websites/$SITE/apps/$APP/wordpress/info" -H "Authorization: Bearer $NBT_TOKEN"
Răspuns
{"id":"c0ffee12-…"}
{"version":"6.8.3","site_url":"https://firma-clientului.ro","plugin_count":3,"user_count":1,"has_woocommerce":false}

Instalarea pornește PHP pe site și își face singură baza de date (pachetul clientului trebuie să aibă loc la mysqlDbs). Durează în jur de un minut; până atunci, …/wordpress/info poate întoarce eroare.

Accesul clientului în panou și SSO

Întâi pune panoul pe domeniul tău (Branding). Clienții tăi sunt în realm-ul tău și intră pe domeniul panoului tău (de exemplu https://panou.agentia-ta.ro), nu pe server.nbt-host.com.

Varianta 1: faci tu contul (fără email)

Exemplu
curl
# loginul clientului, în realm-ul tău (orgId = organizația CLIENTULUI)
LOGIN=$(curl -sS -X POST "$NBT_API/logins?orgId=$CLIENT" \
  -H "Authorization: Bearer $NBT_TOKEN" -H 'Content-Type: application/json' \
  -d "{\"email\":\"ion@firma-clientului.ro\",\"name\":\"Ion Popescu\",\"password\":\"$LOGIN_PASS\"}" | jq -r .id)

# membru cu rolul Owner în organizația clientului
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)

# legătură de intrare unică, de scurtă durată, pe domeniul panoului tău
curl -sS "$NBT_API/orgs/$CLIENT/members/$MEMBER/sso" -H "Authorization: Bearer $NBT_TOKEN"
PHP
<?php
require __DIR__ . '/nbt-api.php';
$client = getenv('CLIENT');

// loginul clientului, în realm-ul tău (orgId = organizația CLIENTULUI)
$login = nbt_api('POST', "/logins?orgId=$client", [
    'email' => 'ion@firma-clientului.ro', 'name' => 'Ion Popescu', 'password' => getenv('LOGIN_PASS'),
])['id'];

// membru cu rolul Owner în organizația clientului
$member = nbt_api('POST', "/orgs/$client/members", ['loginId' => $login, 'roles' => ['Owner']])['id'];

// legătură de intrare unică, de scurtă durată, pe domeniul panoului tău
$link = nbt_api('GET', "/orgs/$client/members/$member/sso")['value'];
header('Location: ' . $link);
Node.js
import { api } from './nbt-api.mjs';
const client = process.env.CLIENT;

// loginul clientului, în realm-ul tău (orgId = organizația CLIENTULUI)
const { id: loginId } = await api('POST', `/logins?orgId=${client}`, {
  email: 'ion@firma-clientului.ro', name: 'Ion Popescu', password: process.env.LOGIN_PASS,
});

// membru cu rolul Owner în organizația clientului
const { id: member } = await api('POST', `/orgs/${client}/members`, { loginId, roles: ['Owner'] });

// legătură de intrare unică, de scurtă durată, pe domeniul panoului tău
const link = await api('GET', `/orgs/${client}/members/${member}/sso`);
console.log(link);
Răspuns
{"id":"a5a86df8-…"}                                   ← POST /logins
{"id":"0c7d…"}                                        ← POST /members
"https://panou.agentia-ta.ro/api/login/sessions/sso?otp=…"   ← GET /sso

Varianta 2: invitația pe email

POST /orgs/{CLIENT}/invites
curl -sS -X POST "$NBT_API/orgs/$CLIENT/invites" \
  -H "Authorization: Bearer $NBT_TOKEN" -H 'Content-Type: application/json' \
  -d '{"email":"ion@firma-clientului.ro","name":"Ion Popescu","roles":["SuperAdmin"]}'

Panoul trimite emailul de la adresa din Setări → Branding, cu legătura pe domeniul panoului tău; clientul își alege parola. Cu "sendEmail":false nu pleacă niciun email și primești legătura în răspuns (201, un șir), s-o trimiți tu. Rolul Owner nu se poate da prin invitație (400): proprietarul îl faci cu varianta 1.

GET/orgs/{CLIENT}/membersMembrii organizației (și invitațiile neacceptate).
POST/logins?orgId={CLIENT}Login nou în realm-ul tău: {"email","name","password"} → {"id"}.
POST/orgs/{CLIENT}/membersMembru: {"loginId":"…","roles":["Owner"]}.
PUT/orgs/{CLIENT}/members/{member_id}Rolurile membrului.
DELETE/orgs/{CLIENT}/members/{member_id}Scoate accesul.
PUT/v2/logins/{login_id}/passwordParolă nouă pentru un login: {"newPassword":"…"}.
GET/orgs/{CLIENT}/members/{member_id}/ssoLegătură unică de intrare (SSO), de scurtă durată, pentru butonul „Intră în panou” din aplicația ta.
GET/v2/orgs/{ORG}/customers/loginsLoginurile clienților tăi.
POST/orgs/{CLIENT}/invitesInvitație pe email (vezi mai sus).

Rolurile: Owner (tot în organizația clientului), SuperAdmin, Sysadmin, Support, Business, SiteAccess (doar site-urile date). Legătura SSO se folosește o dată și expiră repede: o ceri chiar când clientul apasă butonul.

Branding, domeniul panoului, nameservere

Panoul tău are marca și domeniul tău: clienții văd logo-ul, culorile, numele și adresa ta de suport, nu NBT Host. Toate se pot face din panou (Setări → Branding, Setări → Platformă) sau prin API.

POST/orgs/{ORG}/websites?kind=controlPanelDomeniul panoului tău: {"domain":"panou.agentia-ta.ro"}. Înainte: în DNS-ul tău, panou.agentia-ta.ro A 185.230.138.193. Dacă ai deja unul: 409 already_exists — pui numele nou ca alias pe site-ul panoului (POST …/websites/{id}/domains cu "kind":"alias") și îl faci principal (PUT …/domains/primary).
GET/orgs/{ORG}/websites?kind=controlPanelSite-ul panoului, cu domain.id (pentru certificat).
POST/orgs/{ORG}/name-serversNameserverele tale: {"domain":"ns1.agentia-ta.ro"}, apoi la fel pentru ns2.
GET/orgs/{ORG}/branding/settingsSetările de branding.
PUT/orgs/{ORG}/branding/settings/{name}O setare (de ex. sge_from_email = adresa de la care pleacă emailurile panoului, ca șir JSON).
PUT/orgs/{ORG}/branding/logoLogo (multipart, câmpul logo; max. 200 KiB, png / svg / jpg). La fel: inverse-logo, favicon, login-image (max. 2 MiB), inverse-icon.
DELETE/orgs/{ORG}/branding/logoScoate logo-ul (la fel pentru celelalte imagini).
Domeniul panoului și logo-ul
curl -sS -X POST "$NBT_API/orgs/$NBT_ORG/websites?kind=controlPanel" \
  -H "Authorization: Bearer $NBT_TOKEN" -H 'Content-Type: application/json' -d '{"domain":"panou.agentia-ta.ro"}'

curl -sS -X PUT "$NBT_API/orgs/$NBT_ORG/branding/logo" -H "Authorization: Bearer $NBT_TOKEN" -F 'logo=@logo.svg'
  • Certificatul domeniului panoului se pune singur în câteva minute, dacă DNS-ul arată deja spre server; altfel letsencrypt_preflight + letsencrypt pe domain.id (SSL).
  • Nameserverele tale: în zona domeniului tău pui ns1.agentia-ta.ro A 185.230.138.193 și ns2.agentia-ta.ro A 213.199.48.100. Zonele clienților primesc de atunci NS-urile tale, iar clienții le pun la registrar.
  • Pune adresa expeditorului (sge_from_email) pe un domeniu găzduit la noi, ca emailurile panoului (resetarea parolei, invitațiile) să treacă de SPF / DKIM.

Erorile

O eroare are un cod HTTP și, de regulă, un corp JSON: {"code":"…","detail":"…","message":"…"} (detail și message pot lipsi). Programează după code, nu după textul din message.

HTTPcodeCând apare (răspunsuri reale)
400invalid_argumentUn câmp greșit: parolă prea simplă, PHP nepermis, pachet fără allowedPhpVersions, token cu rolul Owner, token în format greșit („invalid length: expected length 32…”).
400— (text)Parametru de interogare greșit, corp text: Query deserialize error: invalid digit found in string (de ex. limit=abc).
401no_session_tokenLipsește antetul Authorization.
403unauthorizedToken greșit, expirat, revocat sau de pe alt IP; ori o resursă din afara organizației tale („Only a reseller or the MO may perform this operation”, „Unable to create website outside of the org”).
403only_mo_allowedOperație rezervată administratorului platformei: force=true, servere, grupuri de servere, limite de memorie / procesor.
403resource_out_of_quotaPeste cotă: al doilea site pe un pachet cu unul, a 6-a căsuță din 5, un pachet de client mai mare decât pachetul tău („The total is 10, the usage is 0, while the requested quota is: 11”).
403non_existent_allowanceO funcție pe care pachetul tău n-o are (de ex. featureSSH).
403suspendedPachetul tău e suspendat: „Cannot perform actions on suspended entity 'subscription'”. Citirile merg, scrierile pe clienți nu.
403not_allowedO funcție neinclusă în pachet (de ex. „Branding not enabled on your subscription”).
404not_founddetail spune ce lipsește: {"code":"not_found","detail":"website"}, "subscription", "backup", "org".
404— (text)Un id care nu e uuid valid în cale: UUID parsing failed: invalid character….
429—Prea multe cereri (vezi Limite). Aștepți Retry-After secunde, sau 2, 4, 8 s, și reiei.
500— (text)„An unknown error occurred, please try again later”: o eroare a serverului sau a unei aplicații de pe site (de ex. lista de module a unui WordPress stricat). Reîncearcă o dată; dacă persistă, ne scrii cu ora și calea.

Limite

  • Cotele pachetului tău (clienți, site-uri, spațiu, căsuțe, baze de date, subdomenii, aliasuri, FTP) sunt totalul pentru toți clienții: GET /orgs/{ORG}/subscriptions → resources[] cu usage și total (fără total = nelimitat). Un pachet de client nu poate trece de ele, iar al N+1-lea site primește 403 resource_out_of_quota, chiar dacă pachetul clientului mai are loc.
  • Consumul se socotește pe folosire: un client ocupă un loc de la crearea organizației lui (nu de la abonare), un site de la creare. O organizație goală, uitată, ține un loc: șterge-o.
  • Memorie și procesor: fiecare site are limitele pachetului tău (GET …/cgroup_limits); nu se pot schimba prin API.
  • Cereri: nu am întâlnit o limită (90 de cereri în 17 secunde, toate 200), dar nu e o garanție: rămâi sub 5 cereri pe secundă, folosește paginarea și tratează 429 cu reîncercare (funcțiile din Autentificare o fac).
  • Emailul trimis are o limită pe oră la nivel de server, împotriva spamului.

Ce NU poți face

  • Root sau SSH — nici tu, nici clienții tăi. Cheile SSH și crontab-urile puse peste pachet le scoatem automat, la verificarea orară.
  • Servere externe — nu poți adăuga servere, alege grupuri de servere sau schimba limitele de memorie și procesor (403 only_mo_allowed). VPS-ul de la Partener 50 / 100 e al clusterului NBT Host.
  • Sub-revânzători — pachetele tale pentru clienți nu pot avea clienți: „Cannot create a plan with customers where sub-reselling it not enabled” (400).
  • Overselling — totalul vândut clienților nu trece de cotele tale.
  • Ștergerea definitivă (force=true) — o facem noi, la cerere.
  • Domenii addon și PostgreSQL — nu sunt incluse.
  • Domenii din alte organizații și subdomeniile lor (de exemplu *.nbt-host.ro) — refuzate cu 403.
  • Clienții, serverele și copiile NBT Host sau ale altor parteneri — tokenul tău vede doar organizația ta și clienții tăi (403 unauthorized).

Întrebări frecvente

Trebuie să cumpăr licență Enhance sau să am contract cu Enhance?

Nu. Licența Enhance e inclusă în prețul pachetului de partener și o asigură NBT Host. Nu ai contract direct cu Enhance: lucrezi în panoul Enhance al NBT Host, sub marca și pe domeniul tău.

Primesc root pe VPS-ul de la Partener 50 / 100? Pot adăuga serverul meu?

Nu. VPS-ul inclus intră în clusterul NBT Host: site-urile clienților tăi rulează pe el, iar actualizările, securitatea și copiile de siguranță rămân la noi. Servere de la alți furnizori nu se pot adăuga.

Ce URL folosesc pentru API dacă am panoul pe domeniul meu?

https://server.nbt-host.com/api. Domeniul panoului tău e pentru oameni (tu și clienții tăi, în browser); integrarea folosește adresa de mai sus.

Am pierdut tokenul. Îl pot vedea din nou?

Nu: panoul îl arată o singură dată. Faci altul și îl ștergi pe cel vechi (Rotirea).

Clienții mei văd undeva NBT Host?

Nu în panou, dacă ți-ai pus domeniul, logo-ul, favicon-ul și adresa de expeditor. Zonele DNS pot avea nameserverele tale în loc de ale noastre.

Ce se întâmplă cu clienții mei dacă abonamentul meu nu e plătit?

La suspendarea pachetului tău se suspendă și clienții tăi (site-urile răspund 404). Tokenul încă citește, dar scrierile primesc 403 suspended. La reactivare revine tot ce s-a suspendat odată cu pachetul.

Există un mediu de test (sandbox)?

Nu separat. Fă-ți un pachet de test cu limite mici și un client de test, apoi șterge-le. Un client de test ocupă un loc din cotă cât există.

Suport și contact

Pentru API, panou și servere ne scrii nouă. Pentru clienții tăi (site-urile, emailul, plângerile lor) tu ești primul contact: le dai tu suport, noi te ajutăm pe tine.

Ca să te ajutăm repede, trimite: metoda și calea (fără token), ora cererii, codul HTTP și corpul răspunsului. Nu ne trimite niciodată tokenul; dacă a ajuns într-un mesaj, revocă-l.