Deweloperzy

Webhooki

Wysyłamy zdarzenie metodą POST na twój serwer w chwili, gdy zamówienie się przesuwa lub oferta się zmienia, więc nie musisz odpytywać. Podpisane, ponawiane przez około dobę i takie samo na każdej trasie API.
Na tej stronie
  1. 1Konfiguracja endpointu
  2. 2Zdarzenie
  3. 3Zdarzenia
  4. 4Podpisy
  5. 5Weryfikacja dostarczenia
  6. 6Ponowienia i kolejność

Konfiguracja endpointu

  1. Dodaj na serwerze trasę https, która odczytuje surowe body i odpowiada 2xx w ciągu 10 sekund. Pracę wykonuj po odpowiedzi.
  2. Utwórz endpoint przez API lub poproś nasz zespół. Zapisz zwrócony sekret whsec_: jest pokazywany raz.
  3. Weryfikuj każde żądanie (poniżej), potem usuwaj duplikaty po id zdarzenia.
  4. Wyślij sobie zdarzenie testowe każdego typu, który obsługujesz.

Zdarzenie

Każde zdarzenie ma tę samą kopertę. data zawiera pola właściwe dla typu.

idstring
Zaczyna się od evt_. Takie samo przy każdym ponowieniu i w nagłówku webhook-id.
typestring
Jedno ze zdarzeń poniżej.
api_versionstring
Wersja, w której zapisano data: wersja twojego endpointu.
created_atstring
Kiedy to się stało, co może być wcześniej, niż to otrzymasz.
testboolean
true dla zdarzeń testowych.
dataobject
ID do pobrania pełnego obiektu i co się zmieniło.
Dostarczenie
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
  }
}

Zdarzenia

order.created

Zamówienie dotarło do nas, z twojego sklepu Shopify lub przez API. Opłacone lub 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

Zamówienie zostało opłacone z salda. Wywołuje się też dla zamówień, które czekały na doładowanie.

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

Zapłaciliśmy sprzedawcy. Pozycje mają status with_seller, dopóki nie dotrą do naszego magazynu.

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

Produkty są w naszym magazynie, sprawdzone, ze zdjęciami. Następne jest pakowanie.

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

Paczka wyszła z numerem śledzenia; opłata za wysyłkę jest ostateczna. Sklep, z którego przyszło zamówienie, też ma numer śledzenia.

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

Sprzedawca zmienił cenę i co zrobiła twoja reguła synchronizacji: przeliczyła cenę, wstrzymała lub powiadomiła.

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

Sprzedawcy skończył się wariant, a na twojej półce go nie ma: ustawiamy go na 0 w twoich kanałach.

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

Nasza kontrola wykryła problem z produktem: zły kolor lub rozmiar, uszkodzenie, brakujący element. Wybierz do respond_by; bez odpowiedzi wstrzymujemy go i piszemy do ciebie.

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"
  }
}

Podpisy

Dostarczenia są podpisywane zgodnie ze Standard Webhooks, więc zweryfikuje je biblioteka, której być może już używasz. Z każdym dostarczeniem przychodzą trzy nagłówki:

NagłówekCo zawiera
webhook-idID zdarzenia.
webhook-timestampKiedy wysłaliśmy tę próbę, w sekundach Unix.
webhook-signaturev1, potem przecinek i base64 HMAC-SHA256 z id.timestamp.body z twoim sekretem. W trakcie rotacji dwa takie, oddzielone spacją.

Klucz to twój sekret po whsec_, zdekodowany z base64. Podpisuj surowe body dokładnie w takiej postaci, w jakiej dotarło; odrzucaj znacznik czasu różniący się o więcej niż pięć minut, żeby przechwyconego żądania nie dało się odtworzyć.

Weryfikacja dostarczenia

Z biblioteką standardwebhooks albo kilkoma liniami własnego kodu:

// 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
})

Ponowienia i kolejność

Wszystko poza 2xx w ciągu 10 sekund to błąd i próbujemy ponownie, za każdym razem z tym samym webhook-id oraz nowym znacznikiem czasu i podpisem:

PróbaPo poprzedniej
1Od razu
25 sekund
35 minut
430 minut
52 godziny
65 godzin
710 godzin
810 godzin, ostatnia
  • Po ostatniej próbie dostarczenie jest nieudane. Wyślij je ponownie z listy dostarczeń.
  • Endpoint, do którego każde dostarczenie przez pięć dni się nie udało, jest wyłączany po e-mailu do ciebie. Ponowne włączenie wysyła to, co ominęło go w ostatnich trzech dniach.
  • Zdarzenia mogą przyjść dwa razy i w innej kolejności. Usuwaj duplikaty po id, a gdy kolejność ma znaczenie, pobierz obiekt i ufaj jego statusowi, a nie statusowi zdarzenia.