Разработчикам

Вебхуки

Мы отправляем событие методом POST на ваш сервер в тот момент, когда заказ движется или карточка меняется, так что опрашивать не нужно. С подписью, с повторами около суток и одинаково на всех маршрутах API.
На этой странице
  1. 1Настройка эндпоинта
  2. 2Событие
  3. 3События
  4. 4Подписи
  5. 5Проверка доставки
  6. 6Повторы и порядок

Настройка эндпоинта

  1. Добавьте на сервер https-маршрут, который читает сырое тело запроса и отвечает 2xx в течение 10 секунд. Обработку выполняйте после ответа.
  2. Создайте эндпоинт через API или попросите нашу команду. Сохраните секрет whsec_ из ответа: он показывается один раз.
  3. Проверяйте каждый запрос (ниже), затем удаляйте дубликаты по id события.
  4. Отправьте себе тестовое событие каждого обрабатываемого типа.

Событие

У всех событий одинаковая оболочка. data содержит поля, свойственные типу.

idstring
Начинается с evt_. Одинаков при каждом повторе и в заголовке webhook-id.
typestring
Одно из событий ниже.
api_versionstring
Версия, в которой записаны данные: версия вашего эндпоинта.
created_atstring
Когда это произошло — может быть раньше, чем вы это получите.
testboolean
true для тестовых событий.
dataobject
ID для получения полного объекта и что изменилось.
Доставка
POST /hooks/ninjabuy HTTP/1.1
Host: acme-goods.com
Content-Type: application/json
User-Agent: NinjaBuy-Webhooks/1.0
webhook-id: evt_4m2x
webhook-timestamp: 1793685600
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=

{
  "id": "evt_4m2x",
  "type": "order.shipped",
  "api_version": "2026-11",
  "created_at": "2026-11-03T06:00:00Z",
  "test": false,
  "data": {
    "order": "ord_3n7q",
    "external_id": "1012",
    "shipment": "shp_5r8w",
    "carrier": "YunExpress",
    "tracking_number": "YT2630812345678",
    "tracking_url": "https://t.17track.net/en#nums=YT2630812345678",
    "shipping_charged": { "amount": "134.52", "currency": "CNY" },
    "pushed_to_channel": true
  }
}

События

order.created

Заказ дошёл до нас — из вашего магазина Shopify или через API. Оплачен или awaiting_payment.

order.created
{
  "id": "evt_1a7c",
  "type": "order.created",
  "api_version": "2026-11",
  "created_at": "2026-11-01T09:14:02Z",
  "test": false,
  "data": {
    "order": "ord_3n7q",
    "external_id": "1012",
    "source": "shopify",
    "status": "buying",
    "payment_status": "paid",
    "lines": [
      {
        "line": "lin_9a2c",
        "variant": "var_41hz",
        "sku": "5187209554",
        "quantity": 1
      }
    ]
  }
}

order.paid

Заказ оплачен с баланса. Срабатывает и для заказов, ждавших пополнения.

order.paid
{
  "id": "evt_1a9f",
  "type": "order.paid",
  "api_version": "2026-11",
  "created_at": "2026-11-01T11:02:40Z",
  "test": false,
  "data": {
    "order": "ord_3n8a",
    "external_id": "1013",
    "amount": { "amount": "57.00", "currency": "CNY" },
    "paid_from": "balance",
    "was_awaiting_payment": true
  }
}

order.bought

Мы заплатили продавцу. Позиции в статусе with_seller, пока не дойдут до нашего склада.

order.bought
{
  "id": "evt_2c4e",
  "type": "order.bought",
  "api_version": "2026-11",
  "created_at": "2026-11-01T13:30:05Z",
  "test": false,
  "data": {
    "order": "ord_3n7q",
    "external_id": "1012",
    "lines": [
      {
        "line": "lin_9a2c",
        "sku": "5187209554",
        "quantity": 1,
        "status": "with_seller"
      }
    ]
  }
}

order.checked

Товары на нашем складе, проверены, с фото. Далее — упаковка.

order.checked
{
  "id": "evt_9w1c",
  "type": "order.checked",
  "api_version": "2026-11",
  "created_at": "2026-11-02T03:20:11Z",
  "test": false,
  "data": {
    "order": "ord_3n7q",
    "external_id": "1012",
    "fulfillment_order": "fo_7k2m",
    "lines": [
      {
        "line": "lin_9a2c",
        "qc": "passed",
        "photos": [
          "https://media.ninjabuy.com/qc/ord_3n7q/1.webp",
          "https://media.ninjabuy.com/qc/ord_3n7q/2.webp"
        ]
      }
    ]
  }
}

order.shipped

Посылка отправлена с трекингом; стоимость доставки окончательная. Трекинг есть и у магазина, из которого пришёл заказ.

order.shipped
{
  "id": "evt_4m2x",
  "type": "order.shipped",
  "api_version": "2026-11",
  "created_at": "2026-11-03T06:00:00Z",
  "test": false,
  "data": {
    "order": "ord_3n7q",
    "external_id": "1012",
    "shipment": "shp_5r8w",
    "carrier": "YunExpress",
    "tracking_number": "YT2630812345678",
    "tracking_url": "https://t.17track.net/en#nums=YT2630812345678",
    "shipping_charged": { "amount": "134.52", "currency": "CNY" },
    "pushed_to_channel": true
  }
}

product.cost_changed

Продавец изменил цену, и что сделало ваше правило синхронизации: пересчитало цену, поставило на паузу или уведомило.

product.cost_changed
{
  "id": "evt_8q0d",
  "type": "product.cost_changed",
  "api_version": "2026-11",
  "created_at": "2026-11-04T02:10:00Z",
  "test": false,
  "data": {
    "product": "prd_1x0a",
    "variant": "var_2b7k",
    "sku": "4410923871",
    "cost": {
      "from": { "amount": "42.00", "currency": "CNY" },
      "to": { "amount": "48.00", "currency": "CNY" }
    },
    "action": "repriced",
    "price": {
      "from": { "amount": "18.99", "currency": "USD" },
      "to": { "amount": "20.99", "currency": "USD" }
    },
    "rule": "rule_default"
  }
}

product.out_of_stock

У продавца закончился вариант, а на вашей полке его нет: мы обнулили его на ваших каналах.

product.out_of_stock
{
  "id": "evt_2b9n",
  "type": "product.out_of_stock",
  "api_version": "2026-11",
  "created_at": "2026-11-04T02:11:30Z",
  "test": false,
  "data": {
    "product": "prd_8f2k",
    "variant": "var_41j0",
    "sku": "5187209555",
    "warehouse_on_hand": 0,
    "channels_updated": [
      {
        "channel": "shopify",
        "shop": "acme-goods.myshopify.com",
        "available": 0
      }
    ]
  }
}

qc.issue

Наша проверка выявила проблему с товаром: не тот цвет или размер, повреждение, недостающая деталь. Выберите до respond_by; без ответа мы удерживаем товар и пишем вам.

qc.issue
{
  "id": "evt_6c3h",
  "type": "qc.issue",
  "api_version": "2026-11",
  "created_at": "2026-11-02T03:20:11Z",
  "test": false,
  "data": {
    "order": "ord_3n7q",
    "external_id": "1012",
    "fulfillment_order": "fo_7k2m",
    "line": "lin_9a2c",
    "sku": "5187209554",
    "issue": "wrong_color",
    "note": "Received grey, ordered black.",
    "photos": ["https://media.ninjabuy.com/qc/ord_3n7q/3.webp"],
    "choices": ["ship_anyway", "exchange", "refund"],
    "respond_by": "2026-11-05T03:20:11Z"
  }
}

Подписи

Доставки подписываются по стандарту Standard Webhooks, так что их проверит библиотека, которую вы, возможно, уже используете. С каждой доставкой приходят три заголовка:

ЗаголовокЧто содержит
webhook-idID события.
webhook-timestampКогда мы отправили эту попытку, в секундах Unix.
webhook-signaturev1, затем запятая и base64 HMAC-SHA256 от id.timestamp.body с вашим секретом. Во время ротации — две подписи через пробел.

Ключ — это ваш секрет после whsec_, декодированный из base64. Подписывайте сырое тело ровно в том виде, в каком оно пришло; отклоняйте метку времени, отличающуюся больше чем на пять минут, чтобы перехваченный запрос нельзя было повторить.

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

С библиотекой standardwebhooks или несколькими строками собственного кода:

// npm install standardwebhooks express
import express from "express"
import { Webhook } from "standardwebhooks"

const webhook = new Webhook(process.env.NINJABUY_WEBHOOK_SECRET) // whsec_...
const app = express()

// The raw body: a parsed and re-serialised body no longer matches.
app.post("/hooks/ninjabuy", express.raw({ type: "application/json" }), (req, res) => {
  let event
  try {
    event = webhook.verify(req.body.toString("utf8"), req.headers)
  } catch {
    return res.sendStatus(400)
  }
  res.sendStatus(204) // answer first, then do the work
  queue.push(event) // deduplicate on event.id: a retry repeats it
})

Повторы и порядок

Всё, кроме 2xx в течение 10 секунд, считается ошибкой, и мы повторяем попытку, каждый раз с тем же webhook-id и новыми меткой времени и подписью:

ПопыткаПосле предыдущей
1Сразу
25 секунд
35 минут
430 минут
52 часа
65 часов
710 часов
810 часов, последняя
  • После последней попытки доставка считается неудачной. Отправьте её снова из списка доставок.
  • Эндпоинт, все доставки на который не удавались пять дней, отключается, а вам приходит письмо. При повторном включении отправляется то, что он пропустил за последние три дня.
  • События могут приходить дважды и не по порядку. Удаляйте дубликаты по id, а когда порядок важен, запросите объект и доверяйте его статусу, а не статусу события.