Токены и аутентификация
Все запросы к API, кроме самого выпуска токена, требуют заголовка:
Authorization: Bearer <token>Токен выдаёт POST /auth/token. Способа два, выбираются параметром grant_type.
Что общего у обоих способов
scopeобязателен. Это список запрашиваемых scope через пробел (meta.read,orders.read…). Какие scope есть на конкретном сайте — видно в каталоге эндпоинтов. Неизвестный scope →invalid_scope.- Токен не может быть шире прав пользователя. У пользователя, от чьего имени выдаётся токен, должно быть право
mxapi_auth_tokenи право MODX, соответствующее каждому запрошенному scope. Иначе —insufficient_permission. - Открытый токен существует ровно один раз — в ответе на выпуск. В базе лежит только
sha256-хэш, восстановить токен нельзя. - Токен непрозрачный (не JWT) и проверяется по базе: отзыв срабатывает мгновенно.
- Минимальное время жизни — 60 секунд: более короткий токен не пережил бы собственную выдачу.
Способ 1. Логин и пароль
grant_type=password — токен по учётным данным пользователя MODX. Заводить заранее ничего не нужно.
curl -X POST 'https://site.ru/mxapi/v1/auth/token' \
-H 'Content-Type: application/json' \
-d '{
"grant_type": "password",
"username": "api_user",
"password": "********",
"scope": "meta.read"
}'grant_type можно не передавать — password подразумевается по умолчанию.
Когда уместен: ручная проверка, отладка, разовые вызовы, сценарии, где пароль вводит живой человек.
Чего у такого токена нет. Клиента за ним не стоит, поэтому не работают ни собственное время жизни, ни IP-фильтр, ни свой лимит частоты, ни allow-list контекстов: время жизни берётся общее (mxapi.token_ttl), а ограничен токен только правами самого пользователя. Плюс вызывающая система вынуждена хранить пароль живого пользователя MODX, и смена пароля ломает интеграцию.
Способ 2. Клиент интеграции
grant_type=client_credentials — предпочтительный способ для машинных интеграций: пароль пользователя наружу не уходит, а доступ можно отключить или перевыпустить, не трогая учётную запись.
curl -X POST 'https://site.ru/mxapi/v1/auth/token' \
-H 'Content-Type: application/json' \
-d '{
"grant_type": "client_credentials",
"client_id": "<client_key>",
"client_secret": "<client_secret>",
"scope": "meta.read orders.read"
}'client_id и client_key
В запросе поле называется client_id, а в админке оно показано как client_key — это одно и то же значение.
Клиент интеграции
Клиент — машинная учётка поверх пользователя MODX: своя пара ключей, свой набор scope, своё время жизни токена. Заводится в админке, на странице того пользователя, от чьего имени будет работать интеграция:
- Пользователи → нужный пользователь → вкладка «mxApi»;
- Добавить. В окне указываются:
- название — для людей: видно в списке клиентов и в журнале;
- scope — списком, сгруппированным по источникам: можно отметить весь источник или отдельные scope. Предлагаются только те, на которые есть права у этого пользователя: выдать больше его прав всё равно нельзя;
- время жизни токена — как на сайте (
mxapi.token_ttl), бессрочно или своё значение в секундах;
- после сохранения показываются
client_keyиclient_secret. Секрет отображается один раз и в базе не хранится (только хэш). Не скопировали — остаётся перевыпуск.
Вкладка видна тем, у кого есть право save_user: кто может сменить пользователю пароль, тот и так выпустит токен от его имени.
В строке клиента доступны изменение, перевыпуск секрета, включение/отключение и удаление.
Перевыпуск секрета
Перевыпуск даёт новый client_secret и по умолчанию не гасит уже выданные токены: плановая ротация не должна ронять работающую интеграцию. Если секрет скомпрометирован — отметьте отзыв выданных токенов в окне перевыпуска: тогда старые токены умирают сразу.
Отключение клиента останавливает всё немедленно: и выпуск новых токенов, и работу уже выданных.
Поля клиента, которых нет в форме
Правятся напрямую в таблице modx_mxapi_client. Принимают массив, JSON или строку через запятую/пробел.
| Поле | Смысл |
|---|---|
allowed_ips | Белый список адресов и подсетей CIDR (IPv4 и IPv6), например 203.0.113.10, 10.0.0.0/8. Пусто — ограничения нет. Проверяется и при выпуске токена, и при каждом вызове: адрес, потерявший доступ, перестаёт работать, не дожидаясь истечения токена. Несовпадение → ip_not_allowed |
contexts | Контексты MODX, разрешённые клиенту. Пусто — только контекст по умолчанию (mxapi.context), * — любой. Нужно на мультисайте: без списка токен интеграции одного сайта работал бы на всех. Несовпадение → context_not_allowed |
rate_limit | Свой лимит запросов в минуту; 0 — общий (mxapi.rate_limit_per_minute) |
scopes | Пустой список означает любой scope (в пределах прав пользователя), а не «ни одного». Форма админки пустым его не оставляет |
X-Forwarded-For и IP-фильтр
За балансировщиком или CDN реальный адрес приходит в X-Forwarded-For, но mxApi учитывает этот заголовок только для адресов из настройки mxapi.trusted_proxies — иначе клиент подделал бы адрес и обошёл фильтр. Если фильтр по IP используется за прокси, перечислите прокси в настройке, иначе в allowed_ips придётся вписывать адрес самого прокси.
Время жизни токена
У клиента три режима (поле token_ttl):
| Режим | Значение в базе | Что происходит |
|---|---|---|
| Как на сайте | 0 | берётся mxapi.token_ttl |
| Своё значение | > 0 | секунды; меньше 60 не бывает |
| Бессрочно | -1 | токен не истекает: expires_in: 0, expires_at: null |
Свой TTL нужен потому, что одно значение на весь сайт заставляет выбирать между ночным обменом с учётной системой (нужен длинный токен) и мобильным приложением (нужен короткий).
Бессрочный режим включается явно и только для интеграций, которые невозможно научить перевыпуску: коробочный обмен, чужой скрипт, оборудование. Плата — секрет, живущий до ручного отзыва. На grant_type=password режим не распространяется: клиента там нет.
Ответ на выпуск токена
{
"success": true,
"data": {
"access_token": "DfT2...",
"token_type": "Bearer",
"expires_in": 86400,
"expires_at": "2026-07-31T09:00:00+00:00",
"scope": "meta.read orders.read",
"user": {}
}
}expires_in: 0 вместе с expires_at: null означают бессрочный токен.
Вызов эндпоинтов
curl 'https://site.ru/mxapi/v1/meta/endpoints' \
-H 'Authorization: Bearer <token>'Заголовки, которые понимает API:
| Заголовок | Назначение |
|---|---|
Authorization: Bearer <token> | обязателен везде, кроме /auth/token |
Content-Type: application/json | тело запроса в JSON |
Idempotency-Key: <строка> | для изменяющих запросов: повтор с тем же ключом вернёт ответ первого успешного вызова и заголовок Idempotency-Replayed: true, операция заново не выполнится |
X-MxApi-Actor: <кто> | кто инициировал вызов на стороне вызывающей системы; пишется в журнал |
X-MxApi-Context: <ключ> | контекст MODX — только для эндпоинтов, которые это допускают, и при включённой настройке mxapi.allow_request_context. См. Права доступа → Контексты |
Границы идемпотентности
Повтор возвращается из журнала, поэтому механизм работает только для изменяющих эндпоинтов и только если ответ первого вызова удалось сохранить: потоковые ответы и тела больше 256 КБ не сохраняются, и такой повтор выполнится заново. Ключ должен быть уникальным для операции — не переиспользуйте один и тот же для разных запросов.
Ответ на успешный вызов несёт заголовки лимита частоты: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset. При превышении — HTTP 429 и код rate_limited; окно — одна минута, считается по клиенту (при grant_type=password — по пользователю, до аутентификации — по IP).
Списочные эндпоинты принимают limit и offset, возвращают meta.total; limit удерживается в границах mxapi.max_limit.
Отзыв токена
curl -X POST 'https://site.ru/mxapi/v1/auth/revoke' \
-H 'Authorization: Bearer <token>'Отзывается именно тот токен, которым выполнен запрос (нужно право mxapi_auth_revoke). Чужие токены через API не отзываются — для этого есть перевыпуск секрета с отзывом токенов и отключение клиента в админке.
Формат ответа
Успех:
{"success": true, "data": {}, "meta": {}}Ошибка:
{"success": false, "error": {"code": "invalid_scope", "message": "…", "details": {}}}Опираться нужно на error.code — он часть публичного контракта; текст message может меняться.
Коды ошибок
| HTTP | Код | Когда |
|---|---|---|
| 400 | invalid_json | тело запроса не JSON-объект |
| 400 | missing_parameter | не передан обязательный параметр |
| 400 | invalid_parameter | параметр не проходит проверку типа или значения |
| 400 | invalid_scope | запрошен scope, которого нет ни у одного эндпоинта |
| 400 | unknown_context | контекст MODX не найден или недоступен |
| 401 | token_required | нет заголовка Authorization: Bearer |
| 401 | invalid_token | токен не найден, либо клиент отключён после выдачи |
| 401 | token_expired | срок действия истёк |
| 401 | token_revoked | токен отозван |
| 401 | credentials_required | не переданы учётные данные при выпуске токена |
| 401 | invalid_credentials | неверные логин/пароль или client_id/client_secret |
| 403 | insufficient_scope | у токена нет нужного scope |
| 403 | insufficient_permission | у пользователя нет права MODX |
| 403 | user_inactive | пользователь неактивен или заблокирован |
| 403 | ip_not_allowed | адрес не входит в allowed_ips клиента |
| 403 | context_not_allowed | контекст не разрешён клиенту либо выбор контекста запросом выключен |
| 404 | route_not_found | нет такого эндпоинта |
| 404 | not_found | объект не найден |
| 405 | method_not_allowed | метод не поддерживается эндпоинтом |
| 429 | rate_limited | превышен лимит запросов в минуту |
| 500 | internal_error | внутренняя ошибка; подробности — в лог MODX, в ответ они попадают только при mxapi.debug |
| 503 | service_disabled | API выключен настройкой mxapi.enabled |
Почему «нет такого пользователя» и «неверный пароль» — одна ошибка
Оба случая отвечают invalid_credentials. Иначе API превращается в проверялку существования логинов.
Журнал обращений
Каждый вызов пишется в modx_mxapi_log: клиент, пользователь, эндпоинт, контекст MODX, маршрут, метод, HTTP-статус, код ошибки, длительность, IP, значение X-MxApi-Actor и ключ идемпотентности.
Успешные чтения по умолчанию не пишутся — журнал нужен для аудита изменений и разбора отказов, а не как счётчик обращений; включаются настройкой mxapi.log_reads. Записи и любые ошибки пишутся всегда. Параметры, в имени которых есть password, secret или token, заменяются на [скрыто].
Старые записи и протухшие токены убираются автоматически, не чаще раза в час — крон не нужен. Срок хранения журнала — mxapi.log_lifetime.
