API для разработчиков

Управляйте магазином на «Океано» из своей программы: товары, остатки, заказы, покупатели. О событиях магазина сообщают вебхуки.

Адрес API: https://api.okeysait.ru/v1  ·  OpenAPI 3.1  ·  JSON

Начать за 3 шага

  1. Создайте ключ

    В панели магазина: «Настройки» → «API и вебхуки» → вкладка «Ключи». Назовите ключ, отметьте нужные права и нажмите «Создать ключ». Ключ oks_… показывается один раз — сохраните его.

  2. Проверьте ключ

    Запрос GET /shop нужен любому ключу и возвращает данные магазина.

    curl
    curl -H "Authorization: Bearer oks_…" https://api.okeysait.ru/v1/shop
    Ответ
    {
      "site": "myshop",
      "name": "Мой магазин",
      "host": "myshop.okeysait.ru",
      "url": "https://myshop.okeysait.ru",
      "currency": "RUB",
      "plan": "free",
      "limits": {"requests_per_minute": 60},
      "key": {"name": "Склад", "scopes": ["products:read", "products:write"]},
      "scopes": ["products:read", "products:write"]
    }
  3. Вызывайте методы

    Ключ передаётся в каждом запросе заголовком Authorization: Bearer oks_…. Все методы — в таблице ниже.

Ключ — только для сервера. Запросы из браузера не разрешены (CORS выключен): ключ, лежащий в странице, видят все. Для витрины в браузере есть Storefront API без ключа.

На самом okeysait.ru ключ с правом orders:write выдаёт только сотрудник КОН: оплаченный заказ там подключает тариф. Владелец магазина получит 403 staff_only.

Формат

  • JSON в UTF-8, поля в snake_case.
  • Деньги — строкой с двумя знаками: "1290.00", валюта RUB. Даты — ISO 8601 с поясом.
  • Списки — {"data": […], "next_cursor": "…"}. Параметры limit (1–250, по умолчанию 50) и cursor: передайте next_cursor из ответа, чтобы получить следующую страницу. null — страниц больше нет.
  • Ошибки — {"error": {"code": "…", "message": "…"}}. Ориентируйтесь на code.

Методы

Право ключа выбирается в панели. GET /shop открыт любому действующему ключу.

МетодПутьПравоЧто делает
GET/shopлюбоеМагазин, тариф, права ключа
GET/productsproducts:readСписок товаров. Фильтры: category_id, sku, visible, updated_since
POST/productsproducts:writeСоздать товар с вариантами, разделами и фото
GET/products/{id}products:readТовар
PATCH/products/{id}products:writeИзменить товар: только переданные поля
DELETE/products/{id}products:writeУдалить товар
PATCH/stockproducts:writeОстатки и цены пачкой по артикулу, до 500 позиций
GET/categoriesproducts:readСписок разделов
POST/categoriesproducts:writeСоздать раздел
GET/categories/{id}products:readРаздел
PATCH/categories/{id}products:writeИзменить раздел
GET/brandsproducts:readСписок брендов
POST/brandsproducts:writeСоздать бренд
GET/brands/{id}products:readБренд
PATCH/brands/{id}products:writeИзменить бренд
GET/ordersorders:readСписок заказов (с контактами покупателя — персональные данные). Фильтры: status, paid, updated_since
POST/ordersorders:writeСоздать заказ (с Idempotency-Key)
GET/orders/{id}orders:readЗаказ (с контактами покупателя — персональные данные)
PATCH/orders/{id}orders:writeСтатус, оплата, заметка менеджера, комментарий
GET/customerscustomers:readСписок покупателей (персональные данные)
GET/customers/{id}customers:readПокупатель

Что важно знать

  • Варианты товара. Цена, артикул и остаток — у варианта. "stock": null — склад не ведётся, количество не ограничено. Остаток — целое от 0 до 1 000 000 000, цена — от 0 до 1 000 000 000.
  • Списки заменяются целиком. В PATCH /products/{id} список variants заменяет прежний: вариант с id обновляется, без id создаётся, отсутствующие в списке удаляются. Пустой variants: [] не принимается. Так же работают category_ids и images (фото передаются ссылками).
  • Разделы и бренды. slug — собственная часть адреса («svechi»); полный адрес с родителями магазин строит сам. Родитель не найден или создаёт цикл — bad_parent. Удаление — только в панели.
  • Остатки пачкой. PATCH /stock, до 500 позиций {sku, stock, price, compare_price}. Ответ по каждой позиции: updated, not_found, bad_sku или nothing_to_update.
  • Забирать только новое. У товаров и заказов есть updated_since — время прошлого опроса в ISO 8601.

Заказы

  • Без дублей. Передайте заголовок Idempotency-Key (до 200 символов, ключ помнится 24 часа). Повтор с тем же телом вернёт тот же ответ с заголовком Idempotent-Replayed: true; с другим телом — 422 idempotency_mismatch; пока первый запрос выполняется — 409 idempotency_conflict.
  • Позиции — по variant_id или sku, quantity от 1 до 9999 (иначе bad_quantity), не больше 100 позиций. Нужно имя, телефон или почта покупателя.
  • Оплата. "paid": true закрывает заказ и списывает остатки, как оплата на сайте. Если товара не хватает — 409 insufficient_stock, ничего не создано и не изменено. Снять оплату нельзя (unpay_not_supported).
  • Уведомления. Объект notify: admin (по умолчанию true) — владелец магазина получает обычное письмо и сообщение о новом заказе; customer (по умолчанию false) — письмо покупателю, если указана его почта.
  • Заметка менеджера — поле manager_note в PATCH; покупателю она не видна.

Примеры

В примерах ключ лежит в переменной окружения OKEANO_KEY.

Создать товар

curl
curl -X POST https://api.okeysait.ru/v1/products \
  -H "Authorization: Bearer $OKEANO_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Свеча «Лес»",
    "category_ids": [3],
    "images": ["https://example.com/forest.jpg"],
    "variants": [{"sku": "CANDLE-F", "price": "1290.00", "stock": 25}]
  }'

Обновить остатки пачкой

curl
curl -X PATCH https://api.okeysait.ru/v1/stock \
  -H "Authorization: Bearer $OKEANO_KEY" \
  -H "Content-Type: application/json" \
  -d '{"items": [
    {"sku": "CANDLE-F", "stock": 18},
    {"sku": "CANDLE-S", "price": "990.00", "compare_price": "1290.00"}
  ]}'
Ответ
{"data": [
  {"sku": "CANDLE-F", "result": "updated", "variants": 1},
  {"sku": "CANDLE-S", "result": "not_found"}
]}

Получить новые заказы

Запомните время начала опроса и в следующий раз передайте его в updated_since. Знак «+» в часовом поясе в адресе кодируйте как %2B (--data-urlencode в curl делает это сам) или пишите время в UTC с буквой Z. Если в ответе next_cursor не null, повторите запрос с cursor.

curl
curl -G https://api.okeysait.ru/v1/orders \
  -H "Authorization: Bearer $OKEANO_KEY" \
  --data-urlencode "updated_since=2026-10-30T09:00:00+03:00" \
  -d limit=100

Создать заказ без риска дубля

curl
curl -X POST https://api.okeysait.ru/v1/orders \
  -H "Authorization: Bearer $OKEANO_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: crm-order-81234" \
  -d '{
    "customer": {"name": "Анна Лесная", "phone": "+79990001122", "email": "anna@example.com", "address": "Москва, ул. Лесная, 1"},
    "items": [{"sku": "CANDLE-F", "quantity": 2}],
    "comment": "Позвоните за час",
    "notify": {"admin": true, "customer": false}
  }'

Ответ 201 — заказ в том же виде, что GET /orders/{id}. Если связь оборвалась, повторите запрос с тем же Idempotency-Key: второй заказ не появится.

Вебхуки

Подключите адрес в панели: «Настройки» → «API и вебхуки» → вкладка «Вебхуки». Укажите адрес https://…, отметьте события и нажмите «Добавить». Секрет whsec_… для проверки подписи показывает кнопка «Секрет». Кнопка «Проверить» отправляет тестовое событие ping, результат виден на вкладке «Журнал».

Что приходит

POST с JSON-телом: номер события, тип, время, магазин и объект в том же виде, что в API. В примере объект сокращён: у заказа в data все поля, как в GET /orders/{id}.

Тело запроса
{
  "id": "evt_1042",
  "type": "order.paid",
  "created_at": "2026-10-30T09:31:07+00:00",
  "shop": {"site": "myshop", "host": "myshop.okeysait.ru"},
  "data": {
    "id": 518,
    "number": "518",
    "paid": true,
    "total": "2580.00"
  }
}
ЗаголовокЗначение
X-Okeano-EventТип события, например order.paid
X-Okeano-DeliveryНомер доставки
X-Okeano-SignatureПодпись t=<unix>,v1=<hex> — как проверить
Content-Typeapplication/json
User-AgentOkeano-Webhooks/1.0

Как работает доставка

  • Ответьте кодом 2xx за 10 секунд — доставка засчитана. Любой другой ответ или тишина — неудача. Редиректы не выполняются.
  • Повторы: через 1 минуту, 5 минут, 30 минут, 2 часа, 6 часов и 12 часов. Всего 7 попыток, потом доставка помечается «не доставлено»; на вкладке «Журнал» её можно повторить вручную.
  • 50 неудач подряд выключают вебхук, владельцу приходит письмо. Когда адрес снова принимает запросы, нажмите «Включить» в панели.
  • «Хотя бы один раз». Одно событие может прийти дважды — отбрасывайте повторы по id. При смене статуса приходят оба события — order.status_changed и order.updated; при оплате к order.paid могут добавиться order.updated и order.status_changed. Отсекайте повторы по id заказа и updated_at. Порядок событий не гарантируется: не рассчитывайте, что order.paid придёт после order.created.
  • Массовые изменения. Больше 200 изменений каталога за одну операцию (или загрузка файлом) дают одно событие catalog.imported вместо события на каждый товар. Если вы подписаны на события товаров или остатков, catalog.imported добавляется автоматически. После него перечитайте каталог через GET /products?updated_since=….
  • Созданное и откатанное. Событие *.created для объекта, которого уже нет (например, заказ отменён из-за нехватки товара), не отправляется.
  • Адрес получателя: только https, порт 443 или 8443, внутренние адреса (localhost, 10.x, 192.168.x и подобные) запрещены. Адрес проверяется при сохранении и перед каждой отправкой.

События

СобытиеКогда приходит, и что в data
order.createdСоздан заказ (сайт, embed.js или API). Заказ с позициями.
order.paidЗаказ оплачен. Заказ. Приходит при оплате всегда; вместе с ним могут прийти order.updated и order.status_changed — отсекайте повторы по id заказа и updated_at.
order.status_changedСменился статус. Заказ. Вместе с ним приходит и order.updated; при оплате (например, через Robokassa) может прийти вместе с order.paid.
order.updatedИзменены данные заказа: статус, покупатель, адрес, заметка, состав. Заказ. Если вы подписаны на order.created, для заказа, созданного в том же запросе, order.updated не приходит.
product.createdСоздан товар. Товар.
product.updatedИзменены товар, варианты, разделы или фото. Товар.
product.deletedТовар удалён. {id, skus}.
stock.changedИзменились остаток или цена вариантов. {items, truncated}: до 200 вариантов с variant_id, product_id, sku, price, compare_price, stock. Больше 200 изменений за запрос приходят одним catalog.imported; truncated всегда false (оставлено для совместимости).
catalog.importedМассовое изменение каталога. {source, counts}: сколько товаров создано, изменено, удалено и сколько остатков изменено.
customer.createdЗарегистрирован покупатель. Покупатель.
feedback.createdСообщение из формы обратной связи. id, name, email, phone, message, created_at.
pingПроверка из панели. {message}.

Проверка подписи

Заголовок X-Okeano-Signature выглядит так: t=1761816667,v1=5f3c…. Подпись v1 — HMAC-SHA256 от строки <t>.<тело> с вашим секретом whsec_…, в hex. Считайте её по сырому телу запроса, а не по разобранному JSON. Отклоняйте запросы, у которых t отличается от текущего времени больше чем на 5 минут.

PHP
// PHP: проверка подписи вебхука Океано
function okeano_webhook_valid($body, $header, $secret, $tolerance = 300) {
    if (!preg_match('/^t=(\d+),v1=([0-9a-f]{64})$/', $header, $m)) return false;
    if (abs(time() - (int)$m[1]) > $tolerance) return false;
    return hash_equals(hash_hmac('sha256', $m[1] . '.' . $body, $secret), $m[2]);
}
$ok = okeano_webhook_valid(
    file_get_contents('php://input'),
    $_SERVER['HTTP_X_OKEANO_SIGNATURE'] ?? '',
    'whsec_…'
);
Node.js
// Node.js: проверка подписи вебхука Океано (тело — сырые байты запроса)
const crypto = require("crypto");
function okeanoWebhookValid(rawBody, header, secret, tolerance = 300) {
  const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header || "");
  if (!m || Math.abs(Date.now() / 1000 - Number(m[1])) > tolerance) return false;
  const want = crypto.createHmac("sha256", secret)
    .update(`${m[1]}.${rawBody}`)
    .digest("hex");
  return crypto.timingSafeEqual(Buffer.from(want), Buffer.from(m[2]));
}

Если подпись не сошлась, ответьте 400: доставка повторится позже. После проверки отвечайте быстро, а тяжёлую работу выполняйте в фоне.

Лимиты и ошибки

ЧтоБесплатный тарифПлатные и свои сайты
Ключей API110
Вебхуков120
Запросов в минуту на ключ60600

Сверх лимита — 429 rate_limited и заголовок Retry-After: через сколько секунд можно повторять. Товаров и разделов столько, сколько позволяет тариф магазина: больше — products_limit и categories_limit.

Коды ошибок

Тело ответа: {"error": {"code": "not_found", "message": "Не найдено"}}. Текст message может уточняться, программе нужен code.

КодЧто значит
bad_json400Тело запроса — не JSON
bad_cursor400Неверный cursor
bad_date400Неверная дата в updated_since, нужен ISO 8601
missing_key401Нужен заголовок Authorization: Bearer oks_…
invalid_key401Ключ не найден или отозван
insufficient_scope403У ключа нет права на это действие
not_found404Не найдено: нет объекта или пути
method_not_allowed405Метод для этого пути не поддерживается
products_limit409Достигнут лимит товаров тарифа
categories_limit409Достигнут лимит разделов тарифа
insufficient_stock409Не хватает товара на складе для оплаты заказа
idempotency_conflict409Запрос с этим Idempotency-Key ещё выполняется
name_required422Нужно название
variants_required422Нужен хотя бы один вариант
bad_variants422Неверный список вариантов или позиций
variant_not_found422Вариант не найден или принадлежит другому товару
bad_price422Цена должна быть числом от 0 до 1 000 000 000
bad_stock422Остаток — целое число или null
bad_images422Фото — список ссылок http(s)
bad_categories422Неверный список разделов
bad_brand422Нет такого бренда
bad_parent422Родительский раздел не найден или создаёт цикл
items_required422Нужен непустой список items
too_many_items422Слишком много позиций за один запрос (остатки — 500, заказ — 100)
customer_required422Нужно имя, телефон или почта покупателя
bad_email422Неверная почта
bad_quantity422Количество — целое число от 1 до 9999
bad_status422Нет такого статуса заказа
unpay_not_supported422Снять отметку оплаты через API нельзя
idempotency_mismatch422Этот Idempotency-Key уже использован с другим запросом
order_failed422, 500Не удалось создать заказ; частично созданного заказа не остаётся
rate_limited429Слишком много запросов, повторите через минуту
brand_failed500Не удалось создать бренд
internal_error500Внутренняя ошибка, попробуйте позже
site_unavailable503Магазин недоступен
service_unavailable503Сервис временно недоступен

Адрес вебхука проверяется при сохранении в панели, возможны ещё коды: bad_url (неверный адрес), https_required (только https://), bad_port (порт 443 или 8443), host_not_found (адреса нет в DNS), private_address (внутренние адреса запрещены).

Для браузера: Storefront API

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

HTML
<div id="shop"></div>
<script src="https://<ваш-магазин>/embed.js" data-target="#shop" async></script>

Тот же JSON-интерфейс доступен напрямую, в том числе из браузера (CORS включён):

Storefront API
GET  https://<магазин>/ajax/embed.php?action=catalog
GET  https://<магазин>/ajax/embed.php?action=product&id=…
POST https://<магазин>/ajax/embed.php?action=order

Подробнее о способах запуска — на странице «CMS для интернет-магазина».

Описание OpenAPI

Полное описание методов, объектов и событий — в файле OpenAPI 3.1. Импортируйте его в Postman, Insomnia или Swagger UI либо сгенерируйте клиент для своего языка.

Выбор города
Ваш город можно выбрать через корзину или в комментариях к заказу