開発者

Webhook

注文が動いたり出品が変わった瞬間にサーバーへイベントをPOSTするので、ポーリングの必要はありません。署名付きで約1日再試行し、APIのすべてのルートで同じです。
このページの内容
  1. 1エンドポイントを設定
  2. 2イベント
  3. 3イベント
  4. 4署名
  5. 5配信を検証
  6. 6再試行と順序

エンドポイントを設定

  1. 生のボディを読み取り、10秒以内に2xxを返すhttpsルートをサーバーに追加してください。処理は応答した後に行います。
  2. APIでエンドポイントを作成するか、当社チームにご依頼ください。返されるwhsec_シークレットを保存してください。表示は一度だけです。
  3. すべてのリクエストを検証し(下記)、イベントのidで重複を除いてください。
  4. 扱う各タイプのテストイベントを自分宛てに送ってください。

イベント

すべてのイベントは同じエンベロープです。data にはタイプ固有のフィールドが入ります。

idstring
evt_ で始まります。すべての再試行で同じで、webhook-idヘッダーにも入ります。
typestring
以下のイベントのいずれか。
api_versionstring
data が書かれるバージョン:エンドポイントのもの。
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 から注文が当社に届きました。paid または 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

出品者のバリエーションが売り切れ、あなたの棚にもありません:チャンネルで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

検品で商品に問題が見つかりました:色やサイズの違い、損傷、部品の欠品。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方式で署名されるので、すでにお使いのライブラリで検証できます。すべての配信に3つのヘッダーが付きます:

ヘッダー保持するもの
webhook-idイベントID。
webhook-timestampこの試行を送信した日時(Unix秒)。
webhook-signaturev1、カンマ、そしてシークレットで計算した id.timestamp.body の base64 HMAC-SHA256。ローテーション中はスペース区切りで2つ。

キーはwhsec_の後のシークレットをbase64デコードしたものです。届いたままの生のボディに署名し、5分以上ずれたタイムスタンプは拒否してください。傍受したリクエストを再送されないようにするためです。

配信を検証

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

再試行と順序

10秒以内に2xx以外が返ると失敗とみなし、再試行します。毎回同じwebhook-idで、新しいタイムスタンプと署名を付けます:

試行前の試行の後
1すぐに
25秒
35分
430分
52時間
65時間
710時間
810時間(最後)
  • 最後の試行の後、配信は失敗になります。配信リストから再送してください。
  • 5日間すべての配信が失敗したエンドポイントは、メールでお知らせした後に無効化されます。再度有効にすると、直近3日間に届かなかったものを送信します。
  • イベントは重複したり順不同で届くことがあります。idで重複を除き、順序が重要な場合はオブジェクトを取得して、イベントではなくそのステータスを信頼してください。