Доступ, роли и выбор сайта
Каждый запрос к API v2 проходит одну и ту же проверку: кто вы (токен или сессия дашборда), к какому сайту обращаетесь и администратор ли вы на нём. Только после этого запрос попадает в ручку. Всё, что описано на этой странице, одинаково для всех эндпоинтов.
Токен API
Токен — op_ и 64 шестнадцатеричных символа, например op_ВАШ_ТОКЕН в примерах этой документации. У пользователя ровно один токен.
Токен не имеет своих прав. Он наследует права владельца целиком, как пароль учётной записи: работает на всех сайтах, где вы администратор, а если вы суперадмин сети — на всей сети. Скоупов, ограничений по сайтам и токенов «только на чтение» нет. Храните его как пароль.
Где взять. Дашборд https://onpress.pro/dashboard/ → меню пользователя → «Настройки» → «API-ключи» (прямой адрес https://onpress.pro/dashboard/settings/api) → блок «Токен API» → «Выпустить токен» (если токен уже есть — «Выпустить заново»). Токен показывается один раз, сразу после выпуска: в базе лежит только его SHA-256, восстановить текст нельзя. В блоке видны префикс (первые символы), дата выпуска и время последнего обращения.
Через API. Тем же, что уже авторизован (сессией дашборда или действующим токеном):
# выпустить заново: в ответе поле token — единственный раз, когда он виден
curl -s -X POST -H "Authorization: Bearer $OP_TOKEN" "$API/me/api-token" | jq '{prefix: .api_token.prefix, token}'
# отозвать: токена у пользователя не остаётся
curl -s -X DELETE -H "Authorization: Bearer $OP_TOKEN" "$API/me/api-token" | jq '{revoked, api_token}'
В дашборде отзыв — там же, блок «Отзыв токена» → «Отозвать». Отозвать токен тем же токеном через API можно: он перестанет работать, как только запрос завершится.
Перевыпуск убивает прежний токен сразу. Всё, что им подписывалось — агенты, скрипты, CI, — перестанет работать на следующем же запросе. Перед перевыпуском убедитесь, что знаете, кто ещё пользуется токеном этой учётной записи.
Как предъявлять. Заголовком Authorization: Bearer op_… (основной способ) или X-OnPress-Key: op_….
curl -s -H "Authorization: Bearer $OP_TOKEN" "$API/site/$SITE/frames" | jq .total
curl -s -H "X-OnPress-Key: $OP_TOKEN" "$API/site/$SITE/frames" | jq .total
Кэш проверки. Сервис запоминает проверенный токен на минуту. Новый токен работает сразу. Перевыпуск и отзыв через /me/api-token сбрасывают этот кэш, но только в том процессе API, который обработал запрос; перевыпуск через первую версию API (POST /wp-json/onpress/v1/api-token) кэш не сбрасывает вовсе. Поэтому старый токен может проходить ещё до минуты — не полагайтесь на мгновенный отзыв. Время последнего обращения (last_used_at) обновляется не чаще раза в минуту.
Тот же токен понимает первая версия API. Сайты сети принимают Authorization: Bearer op_… на ручках /wp-json/onpress/v1/… с теми же правами. Это нужно, например, чтобы получить список своих сайтов (GET /wp-json/onpress/v1/sites) — в v2 такой ручки нет.
Первый токен без браузера. Если у учётной записи есть пароль приложения WordPress (Application Password), выпустить токен можно ручкой первой версии: GET https://ВАШ_ДОМЕН/wp-json/onpress/v1/api-token показывает, есть ли токен, POST — выпускает или перевыпускает, логин и пароль приложения передаются basic-авторизацией (curl -u "логин:пароль приложения"). Это тот же единственный токен пользователя: вызов перевыпускает его.
Сессия дашборда
Дашборд ходит в тот же API не токеном, а сессионной кукой (вход через Firebase: почта с паролем или Google). Для API это равноправный способ: ручки принимают и куку, и токен, а запрос с кукой дополнительно проверяется по Origin — с чужого сайта придёт 403 cross_origin. Агентам и скриптам кука не нужна. Подробно — в дашборде и входе.
Роли
Ролей две, промежуточных нет.
| Роль | Что может |
|---|---|
| Администратор сайта | Всё на своих сайтах: чтение и запись страниц, фреймов, данных, меню, стилей, настроек, языков, SEO, медиа, пользователей сайта |
| Суперадмин сети | То же на любом сайте сети плюс сетевые операции: создать сайт (POST /sites), сменить или обменять домены (POST /sites/{id}/domain), снять сайт с эфира или удалить (DELETE /sites/{id}), вернуть (POST /sites/{id}/restore) |
Членство в сайте без роли администратора не даёт ничего. Подписчик, автор, редактор сайта получают 403 insufficient_role на любой ручке сайта, включая чтение. Администратор определяется так же, как в WordPress: роль administrator или любая роль с возможностью manage_options.
Суперадмины — список сети, в сервисе никто не прибит. Узнать, суперадмин ли вы: GET $API/me → user.is_super_admin.
Главный сайт сети (onpress.pro) не исключение: писать в него может только его администратор. Каждый пользователь сети на главном сайте — подписчик, поэтому свои сайты он администрирует, а главный — нет.
Как выбрать сайт
Основной способ: сайт в пути
https://onpress.pro/api/v2/site/{siteId}/…
siteId — девять знаков из цифр 2–9 и латиницы без похожих букв (нет 0, O, o, 1, l, I), например x9HTLHECq. Выдаётся один раз при создании сайта и не меняется никогда, поэтому его можно хранить в скриптах и закладках. Он случайный: по одному идентификатору нельзя вычислить соседний.
Под префиксом живут все ручки одного сайта: /pages (и /pages/{ref}/translations), /frames, /data, /menus, /media, /users, /docs/sidebar, а также ручки самого сайта без приставки /site: /css, /design, /settings, /front-page, /cache/flush, /languages, /seo, /seo/pages.
curl -s -H "Authorization: Bearer $OP_TOKEN" "$API/site/$SITE/data/sources" | jq '.writable'
curl -s -H "Authorization: Bearer $OP_TOKEN" "$API/site/$SITE/settings" | jq '.settings.blogname'
На этом адресе внутренний номер блога наружу не выходит: вместо поля blog_id в ответе стоит site_id, вместо заголовка X-OnPress-Blog-Id — X-OnPress-Site-Id.
Несуществующий и чужой сайт отвечают одинаково: 404 site_not_found с одним и тем же текстом, будь идентификатор выдуман или принадлежи он сайту, где вы не администратор. Так сеть нельзя перечислить подбором.
Устаревший способ: номер блога или домен
Работает и будет работать, но для нового кода не нужен. Если путь без /site/{siteId}, сайт выбирается по первому найденному источнику в таком порядке:
blog_idв теле запроса;blog_idв строке запроса;- заголовок
X-OnPress-Blog-Id; domainв теле, в строке запроса или заголовкомX-OnPress-Domain(схема иwww.отбрасываются);- ничего не названо — главный сайт сети (номер 1).
curl -s -H "Authorization: Bearer $OP_TOKEN" -H "X-OnPress-Blog-Id: 233" "$API/menus" | jq .theme
curl -s -H "Authorization: Bearer $OP_TOKEN" "$API/menus?domain=test.onpress.pro" | jq .theme
Ответ на таком адресе несёт заголовки X-OnPress-Blog-Id (выбранный номер) и X-OnPress-Blog-Source (blog_id, domain или default). X-OnPress-Blog-Source: default на запросе записи — почти всегда ошибка вашего запроса: сайт не назван, и запись ушла на главный сайт сети. blog_id, который не является положительным целым (abc, 0, 1.5), даёт 400 invalid_blog_id, а не главный сайт. Неизвестный домен — 404 site_not_found. Если вы не администратор выбранного сайта — 403 insufficient_role.
Ручки сети
/sites, /me, /auth, /health к сайту не относятся и префикса не имеют. /sites/{id} принимает в пути и site_id, и номер блога; blog_id, заголовок и domain там не читаются.
Как узнать siteId
GET $API/sites/{номер блога}— карточка сайта, полеsite_id. Номер виден в дашборде в разделе «Сайты».GET https://onpress.pro/wp-json/onpress/v1/sitesс тем же токеном — все ваши сайты с номерами (data[].id), дальше карточка.GET $API/data?blog_id={номер}— список наборов данных, полеsite_idприходит и на устаревшем адресе.POST $API/sitesпри создании сайта возвращаетsite_idсразу.- В дашборде раздел «Данные» показывает адрес ручек сайта с
site_id.
Лимиты
| Что | Предел | Ответ при превышении |
|---|---|---|
| Запросы к API | 120 в минуту на пользователя, скользящее окно | 429 too_many_requests и Retry-After |
| Неудачные входы (кривой или неизвестный токен) | 20 в минуту с одного адреса | 429 too_many_auth_failures и Retry-After |
| Попытки входа в дашборд | 30 в минуту с одного адреса | 429 too_many_requests |
| Тело запроса | 10 МБ | 413 body_too_large |
| Разметка одного фрейма | 2 МБ | 413 frame_too_large |
| Один набор данных | 512 КБ | 413 dataset_too_large |
| Собственный CSS сайта | 256 КБ | 413 css_too_large |
| Файл медиа | 15 МБ (base64 в теле — примерно до 7 МБ файла) | 413 file_too_large |
| Список страниц | до 200 на страницу выдачи | больше — урезается |
| Список медиа | до 100 на страницу выдачи | больше — урезается |
Каждый ответ авторизованной ручки несёт X-RateLimit-Limit, X-RateLimit-Remaining и X-RateLimit-Reset (unix-время, когда освободится самый старый запрос окна). Лимит считается на пользователя, а не на токен: перевыпуск токена счётчик не обнуляет. Счётчик живёт в памяти процесса API — после перезапуска сервиса он обнуляется, это защита от случайного флуда, а не квота.
Форма ответов
Успех — объект с "success": true и полями ручки. На адресе с /site/{siteId}/ в нём site_id, на устаревшем — blog_id.
Ошибка ручки — объект с "success": false, машинным кодом в error, пояснением в message и дополнительными полями конкретной ошибки:
{
"success": false,
"error": "unknown_language",
"message": "language \"de\" does not exist on this site; the site has en, ru",
"lang": "de",
"languages": ["en", "ru"]
}
Ошибка до ручки — авторизация, выбор сайта, неразборчивое тело, несуществующий маршрут — приходит в форме WordPress REST:
{ "code": "site_not_found", "message": "there is no site AAAAAAAAA you can reach with these credentials", "data": { "status": 404 } }
Ветвитесь по error (или code) и HTTP-статусу, а не по тексту: тексты на английском и могут уточняться. Коды, которые могут прийти на любой запрос:
| Код | HTTP | Что значит |
|---|---|---|
missing_token | 401 | нет ни Authorization: Bearer, ни X-OnPress-Key |
invalid_token | 401 | токен неизвестен, отозван или не той формы |
too_many_auth_failures | 429 | слишком много неудачных входов с вашего адреса |
cross_origin | 403 | запрос с кукой сессии пришёл не с адреса дашборда |
invalid_blog_id | 400 | blog_id или X-OnPress-Blog-Id — не положительное целое |
site_not_found | 404 | сайта нет, он не ваш, или домен не из сети |
insufficient_role | 403 | вы не администратор выбранного сайта; у сетевой операции — не суперадмин |
wrong_realm | 409 | запись пришла не в тот контур (см. ниже), ничего не записано |
too_many_requests | 429 | превышен лимит 120 запросов в минуту |
invalid_json | 400 | тело не разбирается как JSON |
body_too_large | 413 | тело больше 10 МБ |
not_found | 404 | нет такого маршрута |
internal_error | 500 | сбой сервиса, подробности в его журнале |
db_error | 500 | база отказала в записи; в db_code код драйвера |
internal_unavailable | 502 | исполнитель на стороне сайта не ответил или не установлен |
Полная таблица по всем ручкам — в кодах ошибок.
Контуры
У одной базы данных два экземпляра API: боевой https://onpress.pro/api/v2/ и внутренний стенд. Боевой работает со всеми сайтами без ограничений — вам нужен только он. Контур видно без записи: GET $API/health отвечает "realm": "prod", а каждый ответ ручки сайта несёт заголовок X-OnPress-Realm. Если запись всё же попала в чужой контур, она отклоняется 409 wrong_realm, в ответе поля blog_realm, instance_realm и адрес, куда слать запрос; ничего не записано.
Проверка себя
curl -s -D - -o /dev/null -H "Authorization: Bearer $OP_TOKEN" "$API/site/$SITE/menus" | grep -i '^x-'
x-onpress-site-id: x9HTLHECq
x-onpress-realm: prod
x-ratelimit-limit: 120
x-ratelimit-remaining: 91
x-ratelimit-reset: 1790368006