Entwickler

Webhooks

Wir senden ein Event per POST an deinen Server, sobald sich eine Bestellung bewegt oder ein Angebot ändert, du musst also nie pollen. Signiert, etwa einen Tag lang wiederholt und auf jeder Route der API gleich.
Auf dieser Seite
  1. 1Einen Endpunkt einrichten
  2. 2Das Event
  3. 3Events
  4. 4Signaturen
  5. 5Eine Zustellung verifizieren
  6. 6Wiederholungen und Reihenfolge

Einen Endpunkt einrichten

  1. Füge deinem Server eine https-Route hinzu, die den Roh-Body liest und innerhalb von 10 Sekunden mit 2xx antwortet. Erledige die Arbeit nach der Antwort.
  2. Leg den Endpunkt mit der API an oder frag unser Team. Speichere das whsec_-Secret aus der Antwort: Es wird nur einmal angezeigt.
  3. Verifiziere jede Anfrage (siehe unten), dann dedupliziere nach der Event-id.
  4. Schick dir selbst ein Test-Event jedes Typs, den du verarbeitest.

Das Event

Jedes Event hat denselben Umschlag. data enthält die Felder des jeweiligen Typs.

idstring
Beginnt mit evt_. Bei jeder Wiederholung gleich und im webhook-id-Header.
typestring
Eines der Events unten.
api_versionstring
Die Version, in der data geschrieben ist: die deines Endpunkts.
created_atstring
Wann es passiert ist, was vor deinem Empfang sein kann.
testboolean
true für Test-Events.
dataobject
IDs, mit denen du das vollständige Objekt holst, und was sich geändert hat.
Eine Zustellung
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
  }
}

Events

order.created

Eine Bestellung ist bei uns angekommen, aus deinem Shopify-Shop oder über die API. Bezahlt oder 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

Eine Bestellung wurde aus deinem Guthaben bezahlt. Wird auch für Bestellungen ausgelöst, die auf eine Aufladung gewartet haben.

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

Wir haben den Verkäufer bezahlt. Die Positionen sind with_seller, bis sie in unserem Lager ankommen.

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

Die Artikel sind in unserem Lager, geprüft, mit ihren Fotos. Als Nächstes wird gepackt.

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

Ein Paket ist mit Sendungsverfolgung unterwegs; die Versandkosten sind final. Der Shop, aus dem die Bestellung kam, hat die Sendungsverfolgung ebenfalls.

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

Ein Verkäufer hat einen Preis geändert, und was deine Sync-Regel daraufhin getan hat: neu bepreist, pausiert oder benachrichtigt.

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

Der Verkäufer ist bei einer Variante ausverkauft und dein Regal hat keine: Wir setzen sie auf deinen Kanälen auf 0.

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

Unsere Prüfung hat ein Problem mit einem Artikel gefunden: falsche Farbe oder Größe, Schaden, ein fehlendes Teil. Entscheide bis respond_by; ohne Antwort halten wir ihn zurück und schreiben dir.

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

Signaturen

Zustellungen werden nach Standard Webhooks signiert, eine Bibliothek, die du vielleicht schon nutzt, verifiziert sie also. Jede Zustellung bringt drei Header mit:

HeaderWas es enthält
webhook-idDie Event-ID.
webhook-timestampWann wir diesen Versuch gesendet haben, in Unix-Sekunden.
webhook-signaturev1, dann ein Komma und der base64-codierte HMAC-SHA256 von id.timestamp.body mit deinem Secret. Während einer Rotation zwei davon, durch ein Leerzeichen getrennt.

Der Schlüssel ist dein Secret nach whsec_, base64-dekodiert. Signiere den Roh-Body genau so, wie er angekommen ist; lehne einen Zeitstempel ab, der mehr als fünf Minuten abweicht, damit eine abgefangene Anfrage nicht wiederholt werden kann.

Eine Zustellung verifizieren

Mit der Bibliothek standardwebhooks oder in ein paar eigenen Zeilen:

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

Wiederholungen und Reihenfolge

Alles außer einem 2xx innerhalb von 10 Sekunden ist ein Fehlschlag, und wir versuchen es erneut, jedes Mal mit derselben webhook-id und neuem Zeitstempel und neuer Signatur:

VersuchNach dem vorherigen
1Sofort
25 Sekunden
35 Minuten
430 Minuten
52 Stunden
65 Stunden
710 Stunden
810 Stunden, der letzte
  • Nach dem letzten Versuch ist die Zustellung fehlgeschlagen. Sende sie erneut aus der Zustellungsliste.
  • Ein Endpunkt, dessen Zustellungen fünf Tage lang alle fehlgeschlagen sind, wird deaktiviert, nachdem wir dir eine E-Mail geschickt haben. Aktivierst du ihn wieder, wird gesendet, was er in den letzten drei Tagen verpasst hat.
  • Events können doppelt und in falscher Reihenfolge ankommen. Dedupliziere nach id, und wenn die Reihenfolge zählt, hol das Objekt und vertrau seinem Status statt dem des Events.