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