mcpField
S
salla-webhooks

salla-webhooks

github.com/sallaapp开发292 次安装2 周前更新
代码仓库

关于 salla-webhooks

salla-webhooks 是用于构建和运行 Salla Webhook 集成的开发技能,指导开发者选择事件、搭建接收端、配置订阅、验证请求并处理重试与幂等。它通过 Partners MCP 执行应用配置和事件订阅操作。

主要功能

  1. 01指导发现事件和载荷结构
  2. 02涵盖 Node.js/Express 与 Laravel/PHP
  3. 03配置 Webhook 接收端和订阅
  4. 04说明签名与令牌验证方式
  5. 05处理重试和幂等请求

适用场景

  1. 01为 Salla 应用搭建 Webhook 接收端
  2. 02订阅应用的店铺事件
  3. 03验证收到的 Webhook 请求
  4. 04在 Salla 集成中实现事件处理

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——它在 Salla 的沙箱中运行(无需验证签名),可获取 context.settings,使用内置身份验证调用 Salla API,并能在操作事件(例如 shipment.creating)上同步运行,此时返回值会影响操作结果。仅当没有 App Function 触发器涵盖该事件时,才使用 Webhook。

工具与 MCP

编码前,请先在 Webhooks 参考文档(https://docs.salla.dev/421119m0.md)中确认实时事件负载架构;切勿假设其结构。**Salla Partners MCP 会执行操作**:

工具操作说明
salla_appsconnect设置应用的 webhook_url、安全策略和自定义标头(密钥:Portal)
salla_eventslist / subscribe订阅店铺事件(order.*、…);应用事件会自动投递

新建的 Webhook 默认使用版本 2。始终显式设置 webhook_security_strategy (signature | token | none)——Partners MCP 现在要求在 salla_apps action=connect 中提供此参数。选择 none 会停用验证:此时 Salla 投递事件时不会发送 Authorization 标头,也不会发送签名,因此没有任何内容可供验证(第 3 步)。由于 none 不执行验证,错误或缺失的验证(例如对 token Webhook 使用 HMAC)在此策略下会无法察觉——在确认“验证有效”之前,请确认当前策略是 token/signature,而不是 none。店铺级管理基础 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 — 探索

  1. 需要哪些事件?(生命周期、订单、商品、发货……)——请在 Webhooks 参考文档(https://docs.salla.dev/421119m0.md)中确认准确的事件名称和负载。
  2. 应用事件还是店铺事件? 应用事件(app.*——安装、授权、更新、卸载、试用、订阅、设置)会自动投递到应用的 webhook_url:默认情况下,应用已订阅自身的应用事件,因此无需订阅——只需设置 webhook_url(salla_apps action=connect)并处理这些事件即可。店铺事件(order.*、product.*、customer.*、cart.*、店铺侧的 shipment.*)需要订阅:可通过 Partners MCP salla_events action=subscribe(应用安装到店铺时的事件),或通过 Admin API POST /webhooks/subscribe 并提供商家令牌(需要 webhooks.read_write scope)。
  3. 使用什么技术栈? Node/Express(@salla.sa/webhooks-actions)还是 Laravel/PHP(Salla CLI)?

salla_events action=subscribe 会替换整个订阅列表——每次调用都要包含所有希望保留的事件(物流应用的 4 个默认发货 Webhook 也要包含)。若要删除所有订阅,请传入 clear_all: true;单独传入空的 events: [] 会被拒绝,因此不能因忘记传入事件而清空订阅。


步骤 1 — 构建 Webhook 服务端

提供两种受支持的路径,两者都使用单个 POST 端点,并从环境变量加载密钥:

  • Node.js / Express,搭配 @salla.sa/webhooks-actions — 官方服务端包。 它会分发事件(Actions.setSecret(secret)、Actions.on(event, cb)、 Actions.checkActions(body, token)),并在内部执行 token 验证 — 其检查逻辑为 if (secret !== this._secret) return;,也就是说,会将 Authorization 的值与密钥进行纯相等比较(步骤 3,token 策略)。事件会路由到 on(<event>, …) 监听器,或基于文件的 Actions/<domain>/<event>.js 处理程序(salla app create-webhook)。
    • 注意 — 仅支持 Express。 该包使用 CommonJS,并通过文件系统动态调用 require() 来分发处理程序,这在 Next.js(或其他)无服务器运行时中无法正常工作。监听器 API 和 token 相等验证模型可移植;文件分发则不行。在无服务器环境中,应手动验证并分发(步骤 3),而不要依赖此包。
  • Laravel / PHP,通过 Salla CLI(salla app create)— 用于搭建服务端;使用 hash_hmac('sha256', $rawBody, $secret) + hash_equals 手动验证,以采用 signature 策略。

关键决策:如果你要手动验证 signature(步骤 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。 采用前,请确认该包适用于你的运行时(参见上文的无服务器环境注意事项)。

完整脚手架(环境变量、Express Pattern A/B、文件处理程序目录结构、PHP 验证): 参见 references/server-setup.md。

检查点:“服务端已启动,单个 POST 端点可接收事件,密钥已从环境变量加载?”


步骤 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.* 等)。

  1. 设置接收端: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 步)。

    密钥同步关卡: 签名密钥需在合作伙伴 Portal 中创建或轮换 (https://portal.salla.partners/apps/{app_id},将其替换为真实 ID),而非通过 MCP 操作——轮换后,旧值会失效。部署前,立即使用 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)。

  2. 仅订阅店铺事件(如果应用只处理应用事件,可跳过此步骤):使用 salla_events action=list、app_id 列出有效的 slug,然后调用 salla_events action=subscribe、 app_id、events: ["order.created", …](店铺端的 slug,而非 app.*)。

店铺级 Webhook(Admin API,商家令牌)

基础 URL 为 https://api.salla.dev/admin/v2;这五项操作都需要 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" }]
}
字段类型说明
eventstring必填。 来自 GET /webhooks/events — 请验证名称
urlstring必填。 必须接受 POST;不能使用 localhost/test. URL
namestring可选的人类可读标签
versionnumber1 或 2;默认为 2 — 仅在使用旧版时传入 1
rulestring可选条件筛选器 — 参见步骤 4
headersarray可选 {key, value}[],随每次投递发送

security_strategy(signature|token)和 secret(当 security_strategy=signature 时必填)不是注册请求体字段 — 请通过 salla_apps action=connect / Portal 设置,或在更新时设置(更新操作接受这两个字段)。

操作方法 + 路径文档
注册POST /webhooks/subscribehttps://docs.salla.dev/5394134e0.md
更新PUT /webhooks/{id}https://docs.salla.dev/10312606e0.md
列出有效项GET /webhookshttps://docs.salla.dev/5394135e0.md
列出所有事件GET /webhooks/eventshttps://docs.salla.dev/5394136e0.md
停用DELETE /webhooks/unsubscribehttps://docs.salla.dev/5394137e0.md
  • 注册会返回 200,以及已创建的 { id, name, event, type, url, version, rule, headers }。 使用相同 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)——两种策略的验证方式_不同_,用错方式会导致所有请求都被拒绝(或放行)。 首先使用 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 时进行衔接——此类投递不包含身份验证,因此应尽快让新版本通过发布审批,而不是接受未经验证的请求。

webhook_security_strategySalla 发送的内容验证方式
tokenAuthorization: <webhook_secret> 标头直接比较:标头值 === 密钥
signaturex-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 规则”):Salla 会根据事件载荷对其进行评估,并且仅在规则为真时才发送,因此你的端点收到的无关请求更少,负载也更低。在注册/更新请求体中传入 rule——筛选在发送方进行,而不是在你的处理程序中进行。规则需要版本 2(421120)。

规则由一个或多个 attribute operator value 条件组成,并通过 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 */
  }
}

从 payload.merchant(顶层)读取店铺 ID,app.store.authorize 事件也不例外。 它不会嵌套在 data 下,因此 payload.data.merchant.id 的值为 undefined。merchant 是用于查找相应访问令牌和刷新令牌的键。

响应与重试规则 — 必须严格遵守(421119):

规则详情
快速确认Salla 等待连接建立并收到 HTTP 响应,最长约 30 秒,之后便会将投递视为失败 — 务必在此之前确认
绝不因耗时操作而阻塞将数据库写入、邮件、外部调用等操作加入队列 — 先响应,再处理
重试行为收到非成功响应或发生超时后,Salla 会重新发送事件 3 次,每次间隔约 5 分钟;成功响应会停止后续重试(文档)
必须保证幂等Webhook 可能会被重复投递 — 务必进行去重

**重试间隔:**遵循文档(421119)— 重试 3 次,每次间隔约 5 分钟,超时时间约为 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);
}

验收标准:“端点完成验证 → 快速返回 200(远低于约 30 秒)→ 异步处理 → 使用 subscription_id 或原始请求体的哈希值进行去重(而非资源 ID、created_at 或重新序列化的对象)?”


步骤 6 — 处理应用生命周期事件

订阅以下所有事件 — 它们能让你的应用与商家状态保持同步:

事件触发时机操作
app.installed首次安装配置资源,设置默认值
app.store.authorize安装或令牌刷新按商家在数据库中保存或更新令牌
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 小时的生产中断。
“我会对请求正文做 HMAC,以验证 token Webhook。”token 使用的是纯相等比较——Authorization 请求头 === 密钥,而不是哈希。对它做 HMAC 会拒绝所有有效交付。验证方式必须与设置的策略相匹配(步骤 3)。
“我解析 JSON 后再验证签名。”HMAC 必须针对 Salla 发送的原始字节执行。先调用 express.json(),请求正文就已经被修改——验证会失败。先捕获原始内容(步骤 3)。
“用 === 比较签名没问题。”简单的字符串比较容易受到时序攻击。签名 HMAC 和 token 相等性检查都应使用时序安全比较(步骤 3)。
“虽然有 App Function 触发器,但我更熟悉 Webhook。”App Function 在 Salla 的沙箱中运行,内置身份验证、设置和同步控制——只要存在对应触发器,就优先使用它(技能开头)。
“我先写入数据库/发送邮件,再返回 200。”Salla 会等待约 30 秒,然后将交付标记为失败并重试 3 次。先确认收到,再异步处理(步骤 5)。
“每个事件只会交付一次,所以不用做幂等处理。”Webhook 可能会多次到达。使用 subscription_id 或原始请求正文的哈希进行去重——不要使用资源 ID 或 created_at(步骤 5)。
“我会从 payload.data.merchant 读取商店 ID。”信封中的 merchant 位于顶层。data.merchant.id 的值是 undefined,因此你保存的商店信息会错误或为空(步骤 5)。
“验证返回 401——肯定是代码有问题。”首先应怀疑密钥不一致:Portal 轮换密钥(或重新连接)后,已部署的 SALLA_WEBHOOK_SECRET ≠ Portal 密钥,这会导致所有交付都被拒绝(步骤 8)。

第 7 步 — 事件参考

事件目录以及每个事件的确切负载架构由 Salla 维护 — 应以文档为准,而不要依赖本技能中固定的列表(事件名称和结构会变化)。可通过两种方式查询名称:

编写处理程序前,务必从上述文档之一确认事件的确切名称(区分大小写)和 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 日志中检查投递尝试和响应代码

使用 webhook.site 测试: 复制一个唯一 URL → 将其设为 Webhook URL → 从演示商店触发事件 → 检查 webhook.site。若出现负载 → 问题出在你的服务器;若没有任何内容 → 问题出在订阅或范围。在同一页面添加自定义标头,以测试标头投递。

Webhook 活动日志: 合作伙伴 Portal → 你的应用 → Webhooks 日志会显示每次投递尝试、HTTP 代码和完整负载 — 调试服务器前先在此处检查。


关键资源

资源URL
Webhooks 文档https://docs.salla.dev/421119m0.md
条件 Webhookshttps://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
Partner Portalhttps://salla.partners
Telegram 社区https://t.me/salladev

评论

支持 Markdown