Market NinjaMarket Ninja

Вебхуки

Асинхронные уведомления о завершении сессии парсинга вместо периодического опроса API — подписка, проверка доставки и пример интеграции с n8n.

Вместо того чтобы опрашивать GET /me/scrapes/{id}, пока status не станет completed, можно подписать URL, и мы сами отправим на него POST-запрос в момент завершения. Пока существует одно событие: scrape_session.completed.

Подписки создаются и управляются на странице настроек расширения (раздел Вебхуки), а не через этот API — эндпоинта POST /webhooks, который можно было бы вызвать самостоятельно, нет. До 5 активных подписок на аккаунт.

Тело события — это указатель, а не сами данные

{
  "event": "scrape_session.completed",
  "user_id": "3e2f9a10-df34-4b8a-9c31-8e1d2f6a7b90",
  "scrape_session_id": "b1f8b6b0-2f7d-4a1a-9a3c-2f6d8e9a1234",
  "status": "completed",
  "stats": { "total": 40, "pending": 0, "processing": 0, "merged": 38, "rejected": 1, "flagged": 1 },
  "url": "https://data.marketninja.ru/v1/me/scrapes/b1f8b6b0-2f7d-4a1a-9a3c-2f6d8e9a1234/products"
}

Мы намеренно не встраиваем спарсенные данные прямо в событие — чтобы их получить, обратитесь по полю url (та же авторизация Bearer, что и везде на этом сайте, и запрос расходует вашу обычную дневную квоту — точно так же, как прямой вызов этого эндпоинта). Причины две: дневная квота на данные проверяется только на этом одном эндпоинте, и компактное тело события работает одинаково независимо от того, было в сессии 1 товар или 10 000.

user_id показывает, какому из ваших аккаунтов Market Ninja принадлежит событие — пригодится, если вы когда-нибудь направите вебхуки нескольких аккаунтов на один и тот же URL (например, общий эндпоинт автоматизации) и вам нужно будет их различать.

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

Каждый запрос содержит два заголовка:

  • X-Marketninja-Event: scrape_session.completed
  • X-Marketninja-Signature: t=1735689600,v1=5257a869e7bcd1d2...

Подпись — это HMAC-SHA256 от ${timestamp}.${rawBody} с использованием секретного ключа, который был показан один раз при создании подписки. Проверяйте её по исходному телу запроса, полученному как есть — никогда по повторно сериализованному JSON.stringify(JSON.parse(body)): это не гарантирует побайтовое совпадение (порядок ключей, пробелы) и молча сломает проверку.

import crypto from "node:crypto";

function isValidSignature(rawBody, signatureHeader, secret) {
  const parts = Object.fromEntries(
    signatureHeader.split(",").map((p) => p.split("="))
  );
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(parts.v1),
    Buffer.from(expected)
  );
}
Проверка подписи не обязательна, но рекомендуется, если ваш эндпоинт делает что-то чувствительное при получении события. Также проверяйте, что t не слишком старый (обычно достаточно допуска в несколько минут), если нужна защита от повторного воспроизведения запроса — на нашей стороне такой допуск не применяется.

Доставка, повторные попытки и когда мы сдаёмся

  • Любой ответ 2xx считается успешной доставкой — тело ответа не читается.
  • Мы не следуем за редиректами — 3xx обрабатывается точно так же, как явная ошибка.
  • Каждая попытка ограничена таймаутом в 5 секунд.
  • Неудачная попытка (сетевая ошибка, таймаут, 429 или 5xx) повторяется по расписанию с задержкой: примерно через 1 мин, 5 мин, 30 мин, 2ч, 6ч, затем через 24ч — до 7 попыток всего, это около полутора суток — после чего конкретная доставка помечается как окончательно неудачная. Если в ответе 429 есть заголовок Retry-After, используется он, а не фиксированное расписание.
  • Любой другой ответ, отличный от 2xx (например, 400, 404), считается окончательной неудачей сразу — мы исходим из того, что повтор не изменит решение вашего эндпоинта.
  • Если у подписки накопится достаточно подряд идущих неудачных попыток, она автоматически отключается — включить отключённую подписку обратно нельзя, можно только создать новую на странице настроек расширения.
Ваш эндпоинт должен быть доступен по публичному адресу https:// — мы отклоняем target_url, который резолвится в приватный/loopback/link-local адрес, уже на этапе создания подписки (и перепроверяем перед каждой доставкой). Для тестирования локального сервера сначала понадобится туннель (ngrok, Cloudflare Tunnel и т.п.).

Пример: запуск LLM-анализа цен в n8n

Частый сценарий: получить уведомление, забрать данные, передать их LLM, отправить результат куда-нибудь. Специфичны для Market Ninja только два узла; всё, что после них — целиком на ваше усмотрение.

  1. Узел Webhook в качестве триггера — HTTP Method: POST, путь любой на ваш выбор. Именно этот URL вы регистрируете как цель подписки.

  2. Узел HTTP Request сразу после него, настроенный на GET-запрос по полю url из пришедшего события, с Bearer-авторизацией:

    • URL: {{ $json.body.url }}
    • Authentication: Generic Credential Type → Bearer Auth, с сохранённым credential, содержащим один из ваших ключей mn_live_....

    Ответ имеет вид { data: [...], meta: {...} } — тот же формат, что всегда возвращает GET /me/scrapes/{id}/products: по одной записи на каждый сабмишен, с product (null, если ещё не слит).

  3. Дальше делайте с $json.data то, что вам действительно нужно — узел LLM для сводки по ценам, уведомление в Slack/Telegram, запись в базу данных, или всё это вместе. Это уже никак не связано с Market Ninja — настраивается как в любом другом workflow n8n.

Узел Webhook в n8n по умолчанию сразу отвечает 200, независимо от того, успешно ли отработает остальной workflow. Статус «доставлено» на нашей стороне подтверждает только то, что ваш узел Webhook получил уведомление — успех самой автоматизации проверяйте по истории выполнения своего workflow.

Last updated on

On this page