NexorPanel

API NexorPanel

Всё, что делает интерфейс панели, делается теми же запросами: интерфейс — обычный клиент этого API, и другого пути у него нет. Поэтому здесь не бывает «в панели можно, а через API нельзя».

Основы

Базовый адрес — та же машина, на которой открывается панель:

https://panel.example.com/api/v1
  • Тело запроса и ответа — JSON в UTF-8; Content-Type: application/json нужен там, где тело есть.
  • Время везде в UTC по RFC 3339: 2026-09-24T08:12:30Z.
  • Язык сообщений выбирается заголовком Accept-Language: русский, украинский, белорусский, английский, немецкий или польский.
  • Слеш на конце не важен: /api/v1/sites и /api/v1/sites/ — один и тот же адрес, и он отвечает сам, а не перенаправляет (перенаправление после POST у части клиентов теряет тело).
  • Версия одна — v1. Внутри неё поля могут добавляться, но не менять смысл; ломающее изменение получит v2.
  • GET /healthz отвечает без ключа: {"status":"ok","version":"…"} — удобно проверять, что обновление доехало.

Ключи API

Программа представляется ключом, а не логином с паролем. Пароль от панели открывает панель целиком и требует второго фактора, а ключ можно выдать на один скрипт, сделать только для чтения, ограничить сроком и отозвать, не трогая ничего другого.

Ключ заводится в панели: Настройки → Ключи API (или POST /api/v1/tokens из сессии). Показывается он один раз — панель хранит только его отпечаток SHA-256. Передаётся заголовком:

Authorization: Bearer nxp_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
  • Не в строке запроса: панель его там не ищет, потому что адрес попадает в журнал веб-сервера, в историю команд и в Referer.
  • Ключ наследует права своей учётной записи и перечитывает их на каждом запросе. Разжалованный администратор перестаёт быть им и по ключу; ключи заблокированной записи перестают работать.
  • Ключ «только для чтения» отвечает на GET, HEAD и OPTIONS, а на остальное — 403 read_only_key. Мониторингу и выгрузке большего не нужно.
  • Ключом нельзя завести ключ — 403 key_by_key: иначе утёкший ключ выпускал бы себе следующий быстрее, чем это заметят.
  • Смена пароля ключи не отзывает. Если пароль меняют из-за утечки, ключи надо отозвать отдельно.
  • Срок — от одного дня до года, 0 — бессрочно. На учётную запись — до 32 ключей (409 too_many_keys).

Тайный вход

У установки может быть своя строка в адресе (NEXOR_ENTRANCE). Тогда все адреса ниже живут не на /api/v1/…, а на /<строка>/api/v1/…, а всё, что приходит мимо, — в том числе и в API — получает пустую 404. Снаружи остаётся только /healthz. Строку сообщает администратор машины: угадать её нельзя, в этом и смысл.

Правила адресов

ЧтоКак
списокGET /api/v1/<раздел>
один объектGET /api/v1/<раздел>/{id}
создатьPOST /api/v1/<раздел>
изменитьPUT /api/v1/<раздел>/{id}
удалитьDELETE /api/v1/<раздел>/{id}
действиеPOST /api/v1/<раздел>/{id}/<действие>

Поля для создания и изменения — те же, что показывает форма панели: заполните её и посмотрите в инструментах браузера, какой JSON ушёл. Форма и API — одно и то же. Списки для форм (машины, версии PHP, адреса, тарифы) отдают адреса *-options.

Списки

Списки устроены одинаково:

GET /api/v1/sites?limit=25&offset=0&search=example

{"items":[…],"total":42,"limit":25,"offset":0}

limit — не больше 200; большее значение возвращает к умолчанию, а не отказывает. search ищет по имени. Некоторые списки сужаются ещё: ?site=126 — по сайту, ?domain=example.org — по почтовому домену. Ответ клиенту содержит только его объекты.

Отказы

Отказ — это JSON с кодом, сообщением и, если отказ про одно поле, именем поля:

{"code":"invalid_input","message":"Домен должен содержать хотя бы одну точку.","field":"domain"}

Разбирать надо code, а не message. Сообщение написано для человека и меняется вместе с языком; код — нет. Самые частые коды:

HTTPКодЧто случилось
400bad_requestзапрос не разобрать
400invalid_inputполе не прошло проверку; field называет его
401unauthenticatedни ключа, ни сессии
401bad_keyключ отозван, просрочен или такого нет
402needs_planвозможность не входит в тариф клиента; в сообщении назван нужный уровень
403forbiddenраздел не для этой роли
403read_only_keyключ только для чтения
403key_by_keyключом ключа не завести
403account_disabledучётная запись заблокирована
403bad_pathпуть за пределами сайта
403bad_originзапрос из браузера пришёл с чужого источника
404not_foundнет такого объекта — или он не ваш
409name_takenимя занято
409cannot_moveсайт нельзя перевести на другую машину правкой поля
409action_pendingпредыдущее изменение ещё выполняется
409too_many_keysключей на учётной записи уже предел
413file_too_bigфайл больше предела загрузки
422bad_archiveархив не распаковать
429rate_limitedслишком часто; подождите
500internalошибка панели; подробности — в её журнале
501not_implementedраздел в этом режиме не работает
501agent_not_toldагенту не поручена эта часть работы; в сообщении названа недостающая строка настроек
502agent_unreachableагент на машине не отвечает
502agent_refusedу панели и агента разные секреты
502agent_too_oldагент старее панели и не знает запроса
502agent_failedагент отказал; подробность — в журнале панели
503no_agentна машине не настроен агент
503not_configuredустановке не назвали, с чем работать (например, NEXOR_IMAP)

Чужой объект и несуществующий отвечают одинаково — 404 not_found: разница в ответах позволила бы пересчитать чужие сайты.

Примеры

Адрес панели и ключ — в переменных:

export NX=https://panel.example.com/api/v1
export KEY=nxp_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
curl -sS -H "Authorization: Bearer $KEY" $NX/version

Список сайтов и их размер на диске

curl -sS -H "Authorization: Bearer $KEY" "$NX/sites?limit=200" |
  jq -r '.items[] | "\(.domain)\t\(.usedMb) МБ"'

Место сайта — три числа: usedMb (каталог), databasesMb и backupsMb. -1 значит «ещё не посчитали», а не ноль.

Завести сайт

curl -sS -X POST -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
  -d '{"domain":"example.org","serverId":1,"active":true,"quotaMb":-1,
       "phpMode":"php-fpm","ssl":true,"letsEncrypt":true,"forceHttps":true,
       "wwwAlias":true,"backupInterval":"daily","backupCopies":7}' \
  $NX/sites

serverId — машина; номер берут из GET /site-options (servers), там же версии PHP и адреса этой машины. Нуль или отсутствие поля — машина панели; сайт клиента всегда ложится туда.

Поставить сайт на паузу и снять с паузы

curl -sS -X POST -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
  -d '{"active":false}' $NX/sites/126/status

Завести клиента с лимитом на сайты

curl -sS -X POST -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
  -d '{"username":"lira","password":"correct horse battery","company":"ООО «Лира»",
       "email":"petr@example.org","active":true,
       "limits":{"webDomain":5,"database":5,"cron":10,"ftpUser":5}}' \
  $NX/clients

Сделать копию сайта прямо сейчас

curl -sS -X POST -H "Authorization: Bearer $KEY" $NX/sites/126/backups

Перезапустить службу на машине — действия: start, stop, restart, reload.

curl -sS -X POST -H "Authorization: Bearer $KEY" $NX/servers/1/services/nginx/restart

Посмотреть, чем занято место

curl -sS -H "Authorization: Bearer $KEY" $NX/servers/1/disk |
  jq -r '.parts[] | "\(.key)\t\(.bytes)"'

Проверить, что всё в порядке — для мониторинга хватает ключа только на чтение:

curl -sS -H "Authorization: Bearer $KEY" $NX/attention |
  jq -r '.items[] | select(.level != "ok") | .text'

Скачать файл сайта

curl -sS -H "Authorization: Bearer $KEY" \
  "$NX/sites/126/files/download?path=/web/index.php" -o index.php

Развернуть сайт из репозитория

curl -sS -X POST -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
  -d '{"url":"https://github.com/owner/repo","ref":"main","webDir":"public"}' \
  $NX/sites/12/deploy

Машина забирает репозиторий и заменяет им папку сайта целиком — не git pull, который останавливается на первом же местном расхождении и оставляет сайт наполовину обновлённым. Ответ — то, что развернулось: {"commit":"a1b2c3d","ref":"main","webDir":"public"}. webDir — папка, которую отдаёт веб-сервер, считая от папки сайта.

Для приватного репозитория в поле key передаётся закрытый ключ развёртывания, а адрес — ssh; панель его не хранит — он живёт на машине во временном файле на время клонирования. Не принимаются: ext::, file:// и локальные пути, нешифрованные git:// и http://, пароль в ссылке. Подмодули не забираются, каталог .git в папку сайта не попадает. Файлы из .gitignore репозитория остаются на месте; что ещё сохранить — перечисляется в .deployignore в папке сайта на сервере.

Все адреса

Адресов: 219. Пути — от адреса панели. Если не отмечено иное, адрес открыт любой вошедшей роли, а клиент видит только свои объекты.

Вход и своя учётная запись

auth/login и auth/2fa существуют для браузера: они ставят cookie, требуют второй фактор и ограничены по частоте. Скрипту нужен ключ.

POST/api/v1/auth/loginбез входа
POST/api/v1/auth/2faбез входа
POST/api/v1/auth/logoutбез входа
GET/api/v1/me
POST/api/v1/me/password
GET/api/v1/2fa
POST/api/v1/2fa/setup
POST/api/v1/2fa/enable
POST/api/v1/2fa/disable
POST/api/v1/2fa/recovery-codes

Ключи API

Ключом нельзя завести ключ (403 key_by_key). На учётную запись — до 32 ключей.

GET/api/v1/tokens
POST/api/v1/tokens
DELETE/api/v1/tokens/{id}

Главная, поиск и состояние

/healthz доступен и при тайном входе и отвечает версией сборки.

GET/healthzбез входа
GET/api/v1/version
GET/api/v1/dashboard
GET/api/v1/search
GET/api/v1/attention
GET/api/v1/notifications
GET/api/v1/plan/features

Сайты

Сайт заводится на машине из serverId (номер берут из site-options) и правкой на другую не переезжает: другой serverId отвечает 409 cannot_move. Удаление снимает конфигурацию, но не трогает файлы.

GET/api/v1/sites
POST/api/v1/sites
GET/api/v1/sites/{id}
PUT/api/v1/sites/{id}
DELETE/api/v1/sites/{id}
POST/api/v1/sites/{id}/status
POST/api/v1/sites/{id}/provision
POST/api/v1/sites/{id}/copy
POST/api/v1/sites/{id}/publish
GET/api/v1/sites/{id}/overview
GET/api/v1/sites/{id}/stats
PUT/api/v1/sites/{id}/stats
GET/api/v1/sites/{id}/shot
GET/api/v1/sites/{id}/logs
GET/api/v1/sites/{id}/logs/{name}
GET/api/v1/sites/children
GET/api/v1/sites/{id}/children
POST/api/v1/sites/{id}/children
PUT/api/v1/sites/{id}/children/{childId}
DELETE/api/v1/sites/{id}/children/{childId}
GET/api/v1/site-options
GET/api/v1/hosting-options

Развёртывание из репозитория

deploy заменяет папку сайта репозиторием целиком. Для приватного репозитория в запросе передаётся закрытый ключ развёртывания; панель его не хранит.

POST/api/v1/sites/{id}/deploy
GET/api/v1/sites/{id}/deploy-key
POST/api/v1/sites/{id}/deploy-key
DELETE/api/v1/sites/{id}/deploy-key

Файлы сайта

Путь не выходит за пределы папки сайта (403 bad_path). Удаление переносит файл в корзину сайта на 30 дней. Большие файлы загружаются по частям.

GET/api/v1/sites/{id}/files
GET/api/v1/sites/{id}/file
PUT/api/v1/sites/{id}/file
GET/api/v1/sites/{id}/files/search
GET/api/v1/sites/{id}/files/size
POST/api/v1/sites/{id}/files/action
POST/api/v1/sites/{id}/files/upload
POST/api/v1/sites/{id}/files/upload/part
GET/api/v1/sites/{id}/files/download
GET/api/v1/sites/{id}/trash
POST/api/v1/sites/{id}/trash/restore
DELETE/api/v1/sites/{id}/trash

SSL-сертификаты

Let's Encrypt включается в самом сайте (letsEncrypt); эти адреса — для своего сертификата.

GET/api/v1/sites/{id}/certificate
PUT/api/v1/sites/{id}/certificate
DELETE/api/v1/sites/{id}/certificate

Резервные копии сайтов

Сайт входит в адрес: номер копии сам по себе не говорит, чья она.

GET/api/v1/sites/{id}/backups
POST/api/v1/sites/{id}/backups
POST/api/v1/sites/{siteId}/backups/{id}/action
POST/api/v1/sites/{siteId}/backups/{id}/offsite
POST/api/v1/sites/{siteId}/backups/{id}/offsite/fetch
GET/api/v1/sites/{siteId}/backups/{id}/download

Базы данных

temp-login открывает phpMyAdmin получасовой учётной записью, заведённой под один вход; пароли баз панель не хранит.

GET/api/v1/databases
POST/api/v1/databases
GET/api/v1/databases/{id}
PUT/api/v1/databases/{id}
DELETE/api/v1/databases/{id}
POST/api/v1/databases/{id}/temp-login
GET/api/v1/database-users
POST/api/v1/database-users
GET/api/v1/database-users/{id}
PUT/api/v1/database-users/{id}
DELETE/api/v1/database-users/{id}

Доступ SFTP, FTP и WebDAV

FTP-доступы — те же записи, что shell: FTP открывается полем ftp у shell-доступа, а DELETE ftp-users/{id} закрывает только FTP.

GET/api/v1/shell-users
POST/api/v1/shell-users
GET/api/v1/shell-users/{id}
PUT/api/v1/shell-users/{id}
DELETE/api/v1/shell-users/{id}
GET/api/v1/ftp-users
POST/api/v1/ftp-users
GET/api/v1/ftp-users/{id}
PUT/api/v1/ftp-users/{id}
DELETE/api/v1/ftp-users/{id}
GET/api/v1/webdav-users
POST/api/v1/webdav-users
GET/api/v1/webdav-users/{id}
PUT/api/v1/webdav-users/{id}
DELETE/api/v1/webdav-users/{id}

Защищённые папки

GET/api/v1/web-folders
POST/api/v1/web-folders
GET/api/v1/web-folders/{id}
PUT/api/v1/web-folders/{id}
DELETE/api/v1/web-folders/{id}
POST/api/v1/web-folders/{id}/users
DELETE/api/v1/web-folders/{id}/users/{userId}

Задания по расписанию

GET/api/v1/cron
POST/api/v1/cron
GET/api/v1/cron/{id}
PUT/api/v1/cron/{id}
DELETE/api/v1/cron/{id}

Почта

Списки сужаются по ?domain=. Транспорты и получатели ретрансляции — только администратору.

GET/api/v1/mail/domains
POST/api/v1/mail/domains
GET/api/v1/mail/domains/{id}
PUT/api/v1/mail/domains/{id}
DELETE/api/v1/mail/domains/{id}
POST/api/v1/mail/domains/{id}/status
GET/api/v1/mail/mailboxes
POST/api/v1/mail/mailboxes
GET/api/v1/mail/mailboxes/{id}
PUT/api/v1/mail/mailboxes/{id}
DELETE/api/v1/mail/mailboxes/{id}
GET/api/v1/mail/mailboxes/{id}/filters
POST/api/v1/mail/mailboxes/{id}/filters
PUT/api/v1/mail/mailboxes/{id}/filters/{filterId}
DELETE/api/v1/mail/mailboxes/{id}/filters/{filterId}
GET/api/v1/mail/forwards
POST/api/v1/mail/forwards
DELETE/api/v1/mail/forwards/{id}
GET/api/v1/mail/lists
POST/api/v1/mail/lists
DELETE/api/v1/mail/lists/{id}
GET/api/v1/mail/lists-mailman
POST/api/v1/mail/lists-mailman
DELETE/api/v1/mail/lists-mailman/{id}
GET/api/v1/mail/transportsадминистратор
POST/api/v1/mail/transportsадминистратор
DELETE/api/v1/mail/transports/{id}администратор
GET/api/v1/mail/relay-recipientsадминистратор
POST/api/v1/mail/relay-recipientsадминистратор
DELETE/api/v1/mail/relay-recipients/{id}администратор
GET/api/v1/mail/contacts
POST/api/v1/mail/contacts
PUT/api/v1/mail/contacts/{id}
DELETE/api/v1/mail/contacts/{id}
GET/api/v1/mail-options

Почта в браузере

У почты в браузере свой вход: права доказывает пароль ящика, а держит запечатанное cookie почты, поэтому номера ящика в адресах нет. mail/mailboxes/{id}/webmail открывает ящик из сессии панели.

POST/api/v1/mail/mailboxes/{id}/webmail
GET/api/v1/webmail/sessionвход в почту
POST/api/v1/webmail/loginвход в почту
POST/api/v1/webmail/openвход в почту
POST/api/v1/webmail/logoutвход в почту
GET/api/v1/webmail/foldersвход в почту
GET/api/v1/webmail/lettersвход в почту
GET/api/v1/webmail/searchвход в почту
GET/api/v1/webmail/letters/{uid}вход в почту
GET/api/v1/webmail/letters/{uid}/attachments/{index}вход в почту
POST/api/v1/webmail/letters/{uid}/seenвход в почту
POST/api/v1/webmail/letters/{uid}/moveвход в почту
POST/api/v1/webmail/sendвход в почту

DNS

GET/api/v1/dns/zones
POST/api/v1/dns/zones
GET/api/v1/dns/zones/{id}
PUT/api/v1/dns/zones/{id}
DELETE/api/v1/dns/zones/{id}
POST/api/v1/dns/zones/{id}/status
POST/api/v1/dns/zones/{id}/dnssec
GET/api/v1/dns/zones/{id}/records
POST/api/v1/dns/zones/{id}/records
PUT/api/v1/dns/records/{id}
DELETE/api/v1/dns/records/{id}
GET/api/v1/dns/zones/{id}/export
POST/api/v1/dns/zones/{id}/import
GET/api/v1/dns/slave-zones
POST/api/v1/dns/slave-zones
PUT/api/v1/dns/slave-zones/{id}
DELETE/api/v1/dns/slave-zones/{id}
GET/api/v1/dns-options

Клиенты и тарифы

Раздел «Клиенты» открыт и реселлеру, но только с его собственными клиентами.

GET/api/v1/clientsадминистратор, реселлер
POST/api/v1/clientsадминистратор, реселлер
GET/api/v1/clients/{id}администратор, реселлер
PUT/api/v1/clients/{id}администратор, реселлер
DELETE/api/v1/clients/{id}администратор, реселлер
POST/api/v1/clients/{id}/passwordадминистратор, реселлер
GET/api/v1/client-optionsадминистратор, реселлер
GET/api/v1/plansадминистратор
POST/api/v1/plansадминистратор
GET/api/v1/plans/{id}администратор
PUT/api/v1/plans/{id}администратор
DELETE/api/v1/plans/{id}администратор

Серверы и службы

Действия со службами: start, stop, restart, reload. Терминал выполняет команду на машине только там, где у агента стоит NEXOR_AGENT_CONSOLE=1.

GET/api/v1/serversадминистратор
GET/api/v1/monitorадминистратор
GET/api/v1/servers/{id}/metricsадминистратор
GET/api/v1/servers/{id}/agentадминистратор
GET/api/v1/servers/{id}/diskадминистратор
GET/api/v1/servers/{id}/servicesадминистратор
POST/api/v1/servers/{id}/services/{name}/{action}администратор
POST/api/v1/servers/{id}/consoleадминистратор
GET/api/v1/servers/{id}/machine-certificateадминистратор
POST/api/v1/servers/{id}/machine-certificateадминистратор

Межсетевой экран и IP-адреса

GET/api/v1/firewallадминистратор
POST/api/v1/firewallадминистратор
PUT/api/v1/firewall/{id}администратор
DELETE/api/v1/firewall/{id}администратор
GET/api/v1/ip-addressesадминистратор
GET/api/v1/server-ipsадминистратор
POST/api/v1/server-ipsадминистратор
PUT/api/v1/server-ips/{id}администратор
DELETE/api/v1/server-ips/{id}администратор

Копии панели, журналы и обновления

update сообщает, совпадают ли версии панели и агентов на всех машинах и есть ли выпуск новее.

GET/api/v1/panel/backupsадминистратор
POST/api/v1/panel/backupsадминистратор
POST/api/v1/panel/backups/uploadадминистратор
GET/api/v1/panel/backups/planадминистратор
PUT/api/v1/panel/backups/planадминистратор
GET/api/v1/panel/backups/{name}/downloadадминистратор
POST/api/v1/panel/backups/{name}/restoreадминистратор
POST/api/v1/panel/backups/{name}/offsiteадминистратор
POST/api/v1/panel/backups/{name}/offsite/fetchадминистратор
DELETE/api/v1/panel/backups/{name}администратор
GET/api/v1/auditадминистратор
GET/api/v1/panel-logадминистратор
GET/api/v1/updateадминистратор
GET/api/v1/notifications/outboundадминистратор
POST/api/v1/notifications/outbound/testадминистратор
GET/api/v1/migration/sourcesадминистратор
POST/api/v1/migration/inspectадминистратор