salla-webhooks について
salla-webhooks は、Salla の Webhook 連携を構築・運用するための開発スキルです。イベント選択、受信サーバーの構築、購読設定、配信検証、再試行と冪等性への対応を案内し、Partners MCP でアプリ設定や購読操作を行います。
主な機能
- 01イベントとペイロードの確認手順
- 02Node.js/Express と Laravel/PHP の構築方法
- 03Webhook 受信先と購読の設定
- 04署名とトークンによる検証方法
- 05再試行と冪等処理への対応
活用シーン
- 01Salla アプリの Webhook 受信先の構築
- 02アプリによるストアイベントの購読
- 03受信した Webhook の検証
- 04Salla 連携へのイベント処理の実装
MCP 対応
Salla Partners MCP を使い、アプリの Webhook URL とセキュリティ方式を設定し、ストアイベントの一覧取得や購読を行います。実行時のサーバーコードが配信を受信して検証します。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ sallaapp/ salla-partners-agent-kit --skill salla-webhooksインストール後、Claude Code で/salla-webhooksを入力して読み込まれたか確認します。
同梱ファイルreferences/operations.mdreferences/server-setup.md
詳細
Salla Webhooks Flow
信頼性の高いWebhook連携を構築します。サーバーを作成し、イベントを購読し、すべての配信を検証して、すばやく応答し、冪等性を保ちます。手順に沿って進め、次に進む前に各ゲートを完了してください。ステップ2ではSalla Partners MCPを使って操作を実行します。サーバーと検証はランタイムコードで行います。
イベントに対応するApp Functionがある場合は、そちらを優先してください。 salla-app-functionsにトリガーがある場合は、WebhookではなくApp Functionを使用してください。Sallaのサンドボックス内で実行されるため(署名の検証は不要)、
context.settingsを取得し、組み込みの認証でSalla APIを呼び出せます。また、shipment.creatingなど、戻り値が処理内容を決めるアクションイベントでは同期的に実行できます。イベントに対応するApp Functionトリガーがない場合に限り、Webhookを使用してください。
ツールとMCP
コードを書く前に、webhooksリファレンス(https://docs.salla.dev/421119m0.md)でイベントのライブペイロードスキーマを確認してください。形状を決めつけないでください。**Salla Partners MCP**は_操作を実行_します。
| ツール | アクション | 内容 |
|---|---|---|
salla_apps | connect | アプリのwebhook_url、セキュリティ戦略、カスタムヘッダーを設定します(シークレット:Portal) |
salla_events | list / subscribe | アプリをストアイベント(order.*など)に購読させます。アプリイベントは自動配信されるため、購読は不要です |
新しいWebhookのデフォルトはバージョン2です。
webhook_security_strategy(signature|token|none)は必ず明示的に設定してください。Partners MCPでは、salla_apps action=connectでの設定が必須になりました。noneを選ぶと検証が無効になります。その場合、SallaはAuthorizationヘッダーも署名も付けずに配信するため、検証するものはありません(ステップ3)。noneでは検証が一切実行されないため、その状態では不正または未設定の検証(例:tokenWebhookに対してHMACを計算すること)が検出されません。「検証が機能している」と信頼する前に、有効な戦略がnoneではなくtokenまたはsignatureであることを確認してください。ストアレベル管理のベースURL(マーチャントトークン):https://api.salla.dev/admin/v2。登録先はPOST /webhooks/subscribeです。 ドキュメント:https://docs.salla.dev/421119m0.md · 条件付き:https://docs.salla.dev/421120m0.md · Node/Expressリポジトリ:https://github.com/SallaApp/webhook-actions-js
ステップ0 — 調査
- どのイベントが必要ですか?(ライフサイクル、注文、商品、配送など)— webhooksリファレンス(https://docs.salla.dev/421119m0.md)で正確な名前とペイロードを確認してください。
- アプリイベントですか、それともストアイベントですか? アプリイベント(
app.*— インストール、認可、更新、アンインストール、トライアル、サブスクリプション、設定)は、アプリのwebhook_urlに自動配信されます。アプリはデフォルトで自身のアプリイベントを購読しているため、購読は不要です。webhook_url(salla_apps action=connect)を設定し、処理してください。ストアイベント(order.*、product.*、customer.*、cart.*、ストア側のshipment.*)は購読が必要です。Partners MCPのsalla_events action=subscribe(ストアにインストールされたアプリのイベント)または、マーチャントトークン(webhooks.read_writeスコープ)を使ったAdmin APIのPOST /webhooks/subscribeで購読できます。 - スタックは何ですか? Node/Express(
@salla.sa/webhooks-actions)またはLaravel/PHP(Salla CLI)ですか?
salla_events action=subscribeは購読リスト全体を置き換えます — 各呼び出しで、維持したいすべてのイベント(配送アプリのデフォルトの配送Webhook 4件も含む)を指定してください。すべての購読を削除するにはclear_all: trueを渡します。空のevents: []だけでは拒否されるため、イベントの指定漏れによって購読を消去することはできません。
Step 1 — Webhook サーバーを構築する
対応している方法は 2 つあります。どちらも POST エンドポイントは 1 つで、シークレットは env から読み込みます。
@salla.sa/webhooks-actionsを使った Node.js / Express — 公式のサーバーサイドパッケージです。 イベントをディスパッチし(Actions.setSecret(secret)、Actions.on(event, cb)、Actions.checkActions(body, token))、トークンの検証を内部で行います。そのチェックはif (secret !== this._secret) return;で、つまりAuthorizationの値とシークレットを単純に 一致比較します(Step 3 のtoken戦略)。イベントはon(<event>, …)リスナー、または ファイルベースのActions/<domain>/<event>.jsハンドラー(salla app create-webhook)にルーティングします。- 注意点 — Express 専用です。 このパッケージは CommonJS で、ファイルシステム上の
動的な
require()を使ってハンドラーをディスパッチします。そのため、Next.js などのサーバーレス ランタイムでは動作しません。リスナー API とトークンの単純一致モデルは移植できますが、 ファイルディスパッチは移植できません。サーバーレス環境では、このパッケージに頼らず、手動で検証し、 ディスパッチしてください(Step 3)。
- 注意点 — Express 専用です。 このパッケージは CommonJS で、ファイルシステム上の
動的な
- Salla CLI を使った Laravel / PHP(
salla app create)— サーバーをスキャフォールディングします。署名 戦略では、hash_hmac('sha256', $rawBody, $secret)とhash_equalsを使って手動で検証します。
重要な判断点:署名を手動で検証する場合(Step 3)、HMAC は未加工のリクエストボディに対して実行する必要があります。JSON ミドルウェアの前に、パースされていないバイト列を取得してください。同じルートで、パース済みボディを使った手動の HMAC チェックを行わないでください。
サーバーサイドのコードを自分で書く前に、公式の
@salla.saパッケージがないか確認してください。npm search "@salla.sa"を実行してください。@salla.sa/webhooks-actionsと併せて、@salla.sa/passport-strategy(OAuth)、@salla.sa/event、@salla.sa/embedded-sdkも利用できます。 採用する前に、そのパッケージがお使いのランタイムに適合するか(上記のサーバーレスに関する注意点)を確認してください。
スキャフォールディングの全手順(env vars、Express Pattern A/B、ファイルハンドラーの構成、PHP の検証方法)については、 references/server-setup.md を参照してください。
確認事項:「サーバーが起動し、単一の POST エンドポイントでイベントを受信し、シークレットを env から読み込めていますか?」
Step 2 — イベントを登録/サブスクライブする
アプリイベントとストアイベントのサブスクリプション(Partners MCP)✅ パートナーアプリ向け
アプリイベントは自動配信されます。webhook_url の設定だけで完了です。 すべての app.* イベント
(インストール、app.store.authorize、更新、アンインストール、app.trial.*、app.subscription.*、
app.settings.updated)は、アプリがデフォルトで自身のアプリイベントをサブスクライブしているため、自動的に
webhook_url に届きます。これらをサブスクライブする必要はありません。ハンドラーの送信先に設定するだけです
(以下のステップ 1)。salla_events action=subscribe の呼び出し(以下のステップ 2)は、アプリが反応する
ストアイベント(order.*、product.* など)専用です。
-
受信先を設定します。
salla_apps action=connect、app_id、webhook_url、webhook_security_strategy(必須 —signature|token|none)、 任意でwebhook_headersを指定します。(署名シークレットはここではなく、Portal で作成・ローテーションします。)- 戦略は毎回明示的に設定してください。省略すると
noneとして扱われます。 Salla はAuthorizationヘッダーも署名も付けずに配信するため、検証するものがなく、ガードはすべてを 無言で通過させてしまいます(レッドフラグ)。 signature→ HMAC、token→ ヘッダーの単純一致、none→ 検証なし(ステップ 3)。
シークレット同期ゲート: 署名シークレットは MCP ではなく、Partner Portal (
https://portal.salla.partners/apps/{app_id}、実際の ID に置き換え)で作成・ローテーションします。 ローテーションすると古い値は無効になります。デプロイの直前に、salla_apps action=get(webhook_secretフィールド)で 現在の値を読み取ってください。以前のセッションのシークレットを再利用したり、ローカルや環境変数の値が 引き続き Salla の値と一致していると仮定したりしないでください。現在の値を、すべてのデプロイ環境 (prod、staging、preview)でSALLA_WEBHOOK_SECRETに設定し、テスト前にデプロイ済みの環境変数 == Portal のシークレット であることを確認してください。一致しない場合、検証に失敗し、すべての配信で 401 が返されます。公開済みの公開アプリ:
salla_apps action=get→app.webhook_config.approvedにwebhook_secretがある場合、次回の承認までは稼働中のストアが引き続きその値で署名します。SALLA_WEBHOOK_SECRET_PREVIOUSにその値を設定してください(毎回、現在の値を読み取ってください。以降のローテーション後も 承認済みの値のままです)。これにより、両方の値を使った検証が可能になります(ステップ 5 の例)。 新しい公開が承認され、approved.webhook_secretがなくなったら_PREVIOUSを削除してください (step-app-config)。 - 戦略は毎回明示的に設定してください。省略すると
-
ストアイベントのみをサブスクライブします(アプリイベントだけを処理するアプリでは、この手順を省略してください)。有効なスラッグを
salla_events action=list、app_idで一覧表示し、続けてsalla_events action=subscribe、app_id、events: ["order.created", …](ストア側のスラッグ —app.*ではありません)を指定します。
ストアレベルの Webhook(Admin API、マーチャントトークン)
ベース URL は https://api.salla.dev/admin/v2 です。5 つの操作すべてに
webhooks.read_write スコープが必要です(読み取りには webhooks.read も使用できます)。
登録ボディ
(5394134):
{
"name": "Order Created Handler",
"event": "order.created",
"url": "https://your-app.com/webhooks",
"version": 2,
"rule": "total > 100",
"headers": [{ "key": "Authorization", "value": "your-secret-value" }]
}
| フィールド | 型 | 備考 |
|---|---|---|
event | string | 必須。 GET /webhooks/events から取得します。名前を確認してください |
url | string | 必須。 POST を受け付ける必要があります。localhost/test. URL は使用できません |
name | string | 任意の人間が読めるラベル |
version | number | 1 または 2。デフォルトは 2 です。レガシーの場合にのみ 1 を指定してください |
rule | string | 任意の条件付きフィルター。ステップ 4 を参照してください |
headers | array | 任意の {key, value}[]。各配信とともに送信されます |
security_strategy(signature|token)と secret(security_strategy=signature の場合に必須)は、登録ボディのフィールドではありません。salla_apps action=connect/Portal、または両方を受け付ける 更新 で設定してください。
| 操作 | メソッド + パス | ドキュメント |
|---|---|---|
| 登録 | POST /webhooks/subscribe | https://docs.salla.dev/5394134e0.md |
| 更新 | PUT /webhooks/{id} | https://docs.salla.dev/10312606e0.md |
| 有効なものを一覧表示 | GET /webhooks | https://docs.salla.dev/5394135e0.md |
| すべてのイベントを一覧表示 | GET /webhooks/events | https://docs.salla.dev/5394136e0.md |
| 無効化 | DELETE /webhooks/unsubscribe | https://docs.salla.dev/5394137e0.md |
- 登録 は、作成された
{ id, name, event, type, url, version, rule, headers }を含む200を返します。 同じ URL で再登録すると、既存の Webhook のイベントが更新されるか、再び有効になります(重複は作成されません)。 - 更新 では、
idをパスセグメントとして指定します。ボディにはname、url、version、rule、headers、security_strategy、secretを含めることができます。 - 有効なものを一覧表示 は、ストアの Webhook と
paginationブロックを返します(webhooks.read)。 - イベントを一覧表示 は、購読可能なイベント名
{ id, label, event }[]を返します(webhooks.read)。 - 無効化 では、クエリパラメーター
idおよび/またはurlで対象を指定します(どちらか一方が必須です)。urlを指定すると、その URL に登録されているすべての Webhook が削除されます。202を返します。
確認:「適切なイベントを購読していますか(salla_events action=list/GET /webhooks で確認)? Webhook URL は登録されていますか(salla_apps action=get →
app.webhook_config.development.webhook_url がデモストアで使用する URL です。公開申請のドラフトにある webhook_url を基準に判断しないでください)? 公開済みのパブリックアプリの場合は、webhook_config.note の内容も伝えてください。approved.differs_from_development が空でない場合、新しい公開が承認されるまで、マーチャントの本番ストアには承認済みの設定が引き続き適用されます。」
ステップ 3 — すべての配信を保護する
接続時に設定した戦略に照らして検証してください(ステップ 2)。2 つの戦略では検証方法が_異なり_、誤った方法を使うと、すべての配信が拒否(または許可)されます。 まず salla_apps action=get でライブのシークレットを読み取ってください(ステップ 2 のシークレット同期ゲート)。SALLA_WEBHOOK_SECRET は環境変数にのみ保持し、ログには出力しないでください。公開済みアプリでローテーション後のシークレットの承認を待っている間は、SALLA_WEBHOOK_SECRET または SALLA_WEBHOOK_SECRET_PREVIOUS の_どちらか_に一致すれば、どちらの戦略でも配信は有効です。ドキュメント:https://docs.salla.dev/421119m0.md
公開済みアプリで戦略を変更しましたか? webhook_config.approved.differs_from_development に webhook_security_strategy が含まれている場合、次の承認まで、ライブストアでは承認済みの戦略が引き続き使われ、デモストアでは新しい戦略が使われます。署名付き配信にはそれぞれ X-Salla-Security-Strategy(Token | Signature)で戦略が示されます。このヘッダーに基づいて各配信を検証しますが、現在の戦略と承認済みの戦略のみを受け入れてください(ヘッダーによって「検証なし」にダウングレードされることがないようにしてください)。none への切り替え、または none からの切り替えでは引き継ぎができません。これらの配信には認証情報が含まれないためです。検証されていないリクエストを受け入れるのではなく、新しい公開内容が速やかに承認されるようにしてください。
webhook_security_strategy | Salla が送信する内容 | 検証方法 |
|---|---|---|
token | Authorization: <webhook_secret> ヘッダー | 単純な値の一致:ヘッダー値 === シークレット |
signature | x-salla-signature = 生のボディの HMAC | HMAC-SHA256(rawBody, secret) をタイミングセーフに比較 |
none | Authorization も署名もなし | 検証するものはありません — 検証は無効です |
戦略 token — 単純な値の一致(HMAC ではありません)
Salla はシークレットをそのまま Authorization ヘッダーで送信します。検証では、そのヘッダー値と webhook_secret を直接比較します。これは @salla.sa/webhooks-actions が内部で行う処理とまったく同じです:if (secret !== this._secret) return; — ハッシュ化も HMAC も行いません。ボディはチェック対象ではないため、JSON を解析した後のボディでも問題ありません。
import { timingSafeEqual } from "crypto";
// `token` strategy: the Authorization header value IS the secret (no "Bearer" prefix,
// no HMAC). Compare it directly to webhook_secret. timingSafeEqual avoids a timing leak;
// the plain-equality model is `received === secret`.
function verifyToken(authHeader: string, secret: string): boolean {
const a = Buffer.from(authHeader ?? "");
const b = Buffer.from(secret);
return a.length === b.length && timingSafeEqual(a, b);
}
戦略 signature — 生のボディの HMAC
Salla はリクエストボディの64 文字の SHA256 HMAC ハッシュを x-salla-signature ヘッダーに追加して送信します(X-Salla-Security-Strategy: Signature も付与されます)。ハッシュは HMAC-SHA256(rawRequestBody, secret) として計算されます。ペイロードを処理または永続化する前に、タイミングセーフな比較で検証してください。HMAC は生のボディに対して計算する必要があります。JSON ミドルウェアで解析される前に、未解析のバイト列を取得してください。
// TypeScript — Web Crypto API (Node 16+, Deno, Cloudflare Workers).
// Pass the RAW request body (the exact bytes Salla sent). HMAC must run on the
// unparsed body, so capture it BEFORE any JSON middleware parses it.
async function verifySignature(
rawBody: string,
signatureHex: string,
secret: string,
): Promise<boolean> {
const key = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(secret),
{ name: "HMAC", hash: "SHA-256" },
false,
["verify"],
);
return crypto.subtle.verify(
"HMAC",
key,
hexToBytes(signatureHex),
new TextEncoder().encode(rawBody),
);
}
function hexToBytes(hex: string): Uint8Array {
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < hex.length; i += 2)
bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
return bytes;
}
カスタムヘッダー
どちらの戦略でも、追加のヘッダー(内部ルーティング、ゲートウェイ認証、マルチテナント)をサポートしています。
"headers": [
{ "key": "X-App-Source", "value": "salla-prod" },
{ "key": "X-Tenant-ID", "value": "merchant-123" }
]
Portal UI、登録/更新 API、または salla_apps action=connect の webhook_headers で設定します。標準的なヘッダー名(例:X-App-Source、X-Tenant-ID)を使い、受け入れ可能な値はPartners MCP に検証させてください。
ゲート:「検証方法は接続時に設定した戦略と一致していますか(token → ヘッダー値の一致、signature → 生のボディの HMAC)。タイミングセーフに実行され、未検証の配信には 401 を返しますか?(none の場合、未検証のままにするのは誤りではなく意図的な選択ですか?)」
ステップ 4 —(任意)条件付き Webhook ルール
条件付き Webhook には rule 文字列(「Salla Rules」)を含めます。Salla はイベントペイロードに対してルールを評価し、ルールが true の場合にのみ配信するため、エンドポイントが受け取る不要な通知と負荷を減らせます。登録/更新の本文に rule を渡します。これはハンドラー内ではなく、送信側でのフィルタリングです。ルールにはバージョン 2(421120)が必要です。
ルールは 1 つ以上の 属性 演算子 値 条件で構成し、AND/OR で結合します。
field = value equality field != value inequality
field > value greater than field < value less than
condition1 AND condition2 condition1 OR condition2
文字列/アラビア語のリテラルはバッククォートで囲み、数値はそのまま記述します。そのイベントのカテゴリに記載された属性のみを使用してください(以下を参照)。サポートされていない属性は一致しません。
例(421120 より):"total > 100" · "payment_method = mada OR price < 50" ·
"status = `active` OR applied_to = `first_order`" ·
"city = `الرياض` AND location != `حي اليرموك`".
各カテゴリで使用できるイベントと属性は固定されています。ルールで使用できるのは、そのカテゴリの属性のみです。
| カテゴリ | フィルタ可能な属性(一部。全一覧は 421120 を参照) |
|---|---|
| 注文 | id、reference_id、total、sub_total、payment_method、status_id、branch_id、coupon_code、shipment_company_id |
| 商品 | id、name、sku、type、price、quantity、status、is_available、brand_id |
| 顧客 | id、first_name、last_name、email、mobile、gender、city、country、location |
| 特別オファー | id、name、offer_type、status、expiry_date |
| カテゴリ | id、name、parent_id、status、sort_order |
| ブランド | id、name、status、custom_url |
| カート | id、subtotal、total、currency、coupon_code、customer_id |
| その他 | parent_id、store_id、customer_id、product_id、order_id、rating、status(イベント review.added) |
カテゴリ別のイベントと属性の完全なリファレンス:https://docs.salla.dev/421120m0.md
ステップ 5 — 迅速に応答し、冪等性を維持する
すべての Webhook はデータを標準エンベロープでラップします(以下の値は例示用です —
実装前に、Partners MCP(salla_events action=list)または Webhook のドキュメントで、正確なエンベロープとイベントごとの data の形状を確認してください):
{
"event": "order.created",
"merchant": 123456789,
"created_at": "2026-05-28T13:34:45+03:00",
"data": {
/* event-specific — verify shape via MCP */
}
}
ストア ID は
payload.merchant(トップレベル)から読み取ってください。app.store.authorizeの場合も同様です。dataの下にはネストされていないため、payload.data.merchant.idはundefinedになります。merchantは、適切なアクセストークン/リフレッシュトークンを検索するためのキーです。
応答と再試行のルール — 必ず遵守してください(421119):
| ルール | 詳細 |
|---|---|
| すばやく応答する | Salla は接続と HTTP 応答を 約 30 秒待ち、その後配信を失敗として扱います — それより十分前に応答してください |
| 時間のかかる処理をブロックしない | DB への書き込み、メール、外部呼び出しはキューに入れてください — まず応答し、その後に処理します |
| 再試行の動作 | 成功以外の応答またはタイムアウトの場合、Salla はイベントを 約 5 分間隔で 3 回再送します。成功すると、それ以降の再試行は停止します(ドキュメント) |
| 冪等性が必須 | Webhook は複数回配信されることがあります — 必ず重複を排除してください |
**再試行間隔:**ドキュメント(421119)に従ってください — 約 5 分間隔で 3 回再試行、タイムアウトは約 30 秒です。 以前の観測では間隔が 30 秒/15 秒/10 秒と記録されていましたが、古い情報の可能性があるため、それを前提にする前に実稼働ストアで確認してください。いずれの場合も、正確なタイミングに依存せず、すばやく応答して重複を排除してください。
// Fast response + async processing (Express). This route uses the `signature` strategy.
// Mount a RAW body parser on this route — NOT a global express.json(), which would
// consume the body and break HMAC. Parse JSON only AFTER the signature checks out.
// (For the `token` strategy, verify req.get("Authorization") === secret instead — a
// parsed body is then fine. See Step 3 for both.)
app.post("/webhooks/salla", express.raw({ type: "*/*" }), async (req, res) => {
const raw = req.body.toString("utf8"); // Buffer from express.raw
const signature = req.get("X-Salla-Signature") ?? "";
// _PREVIOUS is set only while a rotated secret awaits publication approval (Step 2 gate).
const secrets = [
process.env.SALLA_WEBHOOK_SECRET!,
process.env.SALLA_WEBHOOK_SECRET_PREVIOUS,
].filter((s): s is string => !!s);
const checks = await Promise.all(
secrets.map((s) => verifySignature(raw, signature, s)),
);
if (!checks.some(Boolean)) return res.status(401).end();
res.status(200).end(); // respond immediately (well under Salla's ~30s wait)
processWebhookAsync(JSON.parse(raw)).catch(console.error); // parse AFTER verifying
});
// Idempotency — prefer the stable discriminator subscription_id (unique per lifecycle
// event); otherwise hash the RAW request body (the exact bytes Salla sent), NOT a
// re-stringified object. JSON.stringify(payload) is brittle — key order and number/date
// re-serialization can change the hash for the same delivery. Pass `rawBody` from the same
// express.raw() buffer you verified the signature against.
async function handleWebhook(
payload: WebhookPayload,
rawBody: string,
): Promise<void> {
const discriminator =
(payload.data as any)?.subscription_id ??
crypto.createHash("sha256").update(rawBody).digest("hex").slice(0, 16);
const key = `${payload.merchant}:${payload.event}:${discriminator}`;
const seen = await db.webhookEvents.exists({ key });
if (seen) return; // already processed
await db.webhookEvents.insert({ key, received_at: new Date() }); // insert BEFORE processing
await processEvent(payload);
}
判定基準:「エンドポイントが検証を行い → すばやく(約 30 秒を大幅に下回る時間で)200 を返し → 非同期で処理し → subscription_id または生の本文のハッシュで重複を排除している(リソース ID、created_at、再文字列化したオブジェクトは使っていない)?」
ステップ 6 — アプリのライフサイクルイベントを処理する
これらすべてをサブスクライブしてください — マーチャントの状態とアプリを同期させるために必要です:
| イベント | 発生時期 | 対応内容 |
|---|---|---|
app.installed | 初回インストール時 | リソースをプロビジョニングし、デフォルトを設定します |
app.store.authorize | インストール時またはトークン更新時 | マーチャントごとに DB へトークンを保存/更新します |
app.updated | マーチャントがアプリを更新したとき | Salla は直後に app.store.authorize を発火します — そのイベントを待ってください |
app.trial.started | トライアル開始時 | トライアル機能を有効にします |
app.subscription.started | 有料プラン有効化時 | 有効として記録し、有料機能を解放します |
app.trial.expired | トライアル終了、アップグレードなし | アクセスを制限します |
app.subscription.expired | 有料プラン失効時 | アクセスを制限し、マーチャントに通知します |
app.uninstalled | マーチャントがアプリを削除したとき | 保持ポリシーに従ってマーチャントのデータをクリーンアップします |
app.store.authorize のペイロード(形状は例示用です — 正確なフィールドは MCP/ドキュメントで確認してください):
{
"event": "app.store.authorize",
"merchant": 123456789,
"data": {
"access_token": "...",
"refresh_token": "...",
"expires": 1234567890,
"scope": "orders.read_write products.read_write"
}
}
ライフサイクルの全処理 → salla-app-lifecycle;トークンの保存 → salla-app-auth。
要注意な落とし穴
Webhook サーバーでは無害に見えても、本番環境で気づかないうちにデータ損失やセキュリティ上の問題を引き起こす近道です。該当する項目を実行しようとしている場合は、記載されているステップを読み直してください。
| つい考えてしまうこと | それが間違っている理由 |
|---|---|
「接続時に webhook_security_strategy を未設定のままにしておこう。」 | 未設定の場合は none として扱われます。そのため Salla は Authorization も署名も送信せず、ガードは何も検証しないまま、すべてのリクエストを許可してしまいます。必ず明示的に設定してください(ステップ 2)。この落とし穴により、本番環境で約 1 時間の停止が発生しました。 |
「token Webhook を検証するために、本文に HMAC を適用しよう。」 | token は 単純な一致判定です。Authorization ヘッダーはシークレットと === で一致するものであり、ハッシュではありません。HMAC を適用すると、すべての有効な配信が拒否されます。設定した方式に合わせて検証してください(ステップ 3)。 |
| 「JSON を解析してから署名を検証しよう。」 | HMAC は Salla が送信した 生のバイト列に対して実行する必要があります。先に express.json() を実行すると、本文はすでに変更されているため、検証に失敗します。先に生データを取得してください(ステップ 3)。 |
「署名の比較には === で十分だ。」 | 単純な文字列比較はタイミング攻撃の対象になります。署名の HMAC と token の一致判定の両方に、タイミング攻撃を防ぐ比較を使用してください(ステップ 3)。 |
| 「App Function のトリガーはあるけれど、Webhook のほうが使い慣れている。」 | App Function は Salla のサンドボックス内で実行され、認証、設定、同期制御が組み込まれています。トリガーがある場合は、App Function を優先してください(スキルの冒頭)。 |
| 「DB への書き込みやメール送信を済ませてから、200 を返そう。」 | Salla は約 30 秒待機した後、配信を失敗としてマークし、3 回再試行します。まず応答し、その後に非同期で処理してください(ステップ 5)。 |
| 「各イベントは一度だけ配信されるので、冪等性は省略してよい。」 | Webhook は複数回届く場合があります。リソース ID や created_at ではなく、subscription_id または本文の生データのハッシュで重複を排除してください(ステップ 5)。 |
「ストア ID は payload.data.merchant から読み取ろう。」 | merchant はエンベロープの トップレベルにあります。data.merchant.id は undefined となり、誤ったストアや空のストアを保存してしまいます(ステップ 5)。 |
| 「検証で 401 が返る。コードのバグに違いない。」 | まずシークレットが一致しているかを確認してください。Portal でシークレットをローテーションした後(または再接続後)に、デプロイ済みの SALLA_WEBHOOK_SECRET と Portal のシークレットが異なると、すべての配信が拒否されます(ステップ 8)。 |
ステップ 7 — イベントリファレンス
イベントカタログと各イベントの正確なペイロードスキーマはSalla が管理しています。イベント名や構造は変更されるため、このスキルに組み込まれたリストではなく、ドキュメントを信頼できる情報源として扱ってください。イベント名は次の 2 つの方法で確認できます。
-
リアルタイムでプログラムから確認:
salla_events action=list(app_id)を実行すると、アプリが現在登録できるイベントが返されます。 -
ドキュメント: ストアイベントとイベントごとのペイロードスキーマはドメイン別に分かれています。必要なドメインのページを使用してください(トランスポート + 概要:https://docs.salla.dev/421119m0.md)。
ハンドラーを書く前に、必ずいずれかのドキュメントで正確なイベント名(大文字と小文字を区別)と data の構造を確認してください。イベントのペイロード(または登録/更新のボディ)がドキュメント化されている場合(docs.salla.dev/<id>.md ページの OpenAPI ブロック。salla-docs から検索できます)、そのスキーマに照らしてハンドラーの解析処理と登録ボディを検証し、依存する前に修正してください。その際は salla-api-core のスキーマ読み取り → 構築 → 検証 → 修正 → 再試行のループを使用します。
ステップ 8 — トラブルシューティング
Webhook が届かない場合:
- すべての配信が 401 を返す → まずシークレットの一致を確認してください。 デプロイ済みの
SALLA_WEBHOOK_SECRETは Portal のシークレットと一致している必要があります。Portal でのローテーション(または再接続)により、新しいシークレットが生成されます。この不一致だけで、すべての Webhook が拒否されます。公開済みの公開アプリでは、ライブストアは次の承認まで最後に承認されたシークレットを使用します。シークレット同期ゲートを参照してください。 - デモストアには届くが、ライブストアには届かない(または古い URL/イベントが使われる)場合は? ライブストアは、新しい公開リクエストが承認されるまで、承認済み公開の設定を使用します。
salla_apps action=get→app.webhook_config.approved.differs_from_developmentに相違点(確認できなかった項目はnot_compared)が表示されます。webhook_config.noteに対応方法が記載されています。 - Webhook URL が設定され、
webhooks.read_writeスコープが有効になっている - アプリがデモストアにインストールされている(必要に応じて、「インストール済みアプリ」から先にアンインストールして再インストールする)
- 正しいイベント名(大文字と小文字を区別)を登録している
- エンドポイントが Salla の約 30 秒の待機時間を大幅に下回る時間で
200/201を返し、POST を受け付ける(GET だけではない) - サーバーに TLS/SSL の問題がない
- Portal の Salla Webhooks Log で配信の試行とレスポンスコードを確認する
webhook.site でテストする: 固有の URL をコピー → Webhook URL として設定 → デモストアからイベントを発生させる → webhook.site を確認します。ペイロードが表示される場合はサーバーに問題があります。何も表示されない場合は、登録またはスコープに問題があります。同じページでカスタムヘッダーを追加すると、ヘッダーの配信もテストできます。
Webhook アクティビティログ: Partners Portal → Your App → Webhooks Log には、すべての配信試行、HTTP コード、ペイロード全体が表示されます。サーバーのデバッグを行う前に、ここを確認してください。
主要なリソース
| リソース | URL |
|---|---|
| Webhook のドキュメント | https://docs.salla.dev/421119m0.md |
| 条件付き Webhook | https://docs.salla.dev/421120m0.md |
Webhook の登録(POST /webhooks/subscribe) | https://docs.salla.dev/5394134e0.md |
Webhook の更新(PUT /webhooks/{id}) | https://docs.salla.dev/10312606e0.md |
有効な Webhook の一覧(GET /webhooks) | https://docs.salla.dev/5394135e0.md |
イベントの一覧(GET /webhooks/events) | https://docs.salla.dev/5394136e0.md |
無効化(DELETE /webhooks/unsubscribe) | https://docs.salla.dev/5394137e0.md |
| Node.js/Express リポジトリ | https://github.com/SallaApp/webhook-actions-js |
| 操作(再試行、カスタムヘッダー、CLI によるローカル開発) | references/operations.md |
| パートナーポータル | https://salla.partners |
| Telegram コミュニティ | https://t.me/salladev |