关于 salla-webhooks
salla-webhooks 是用于构建和运行 Salla Webhook 集成的开发技能,指导开发者选择事件、搭建接收端、配置订阅、验证请求并处理重试与幂等。它通过 Partners MCP 执行应用配置和事件订阅操作。
主要功能
- 01指导发现事件和载荷结构
- 02涵盖 Node.js/Express 与 Laravel/PHP
- 03配置 Webhook 接收端和订阅
- 04说明签名与令牌验证方式
- 05处理重试和幂等请求
适用场景
- 01为 Salla 应用搭建 Webhook 接收端
- 02订阅应用的店铺事件
- 03验证收到的 Webhook 请求
- 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_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)在此策略下会无法察觉——在确认“验证有效”之前,请确认当前策略是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 — 探索
- 需要哪些事件?(生命周期、订单、商品、发货……)——请在 Webhooks 参考文档(https://docs.salla.dev/421119m0.md)中确认准确的事件名称和负载。
- 应用事件还是店铺事件? 应用事件(
app.*——安装、授权、更新、卸载、试用、订阅、设置)会自动投递到应用的webhook_url:默认情况下,应用已订阅自身的应用事件,因此无需订阅——只需设置webhook_url(salla_apps action=connect)并处理这些事件即可。店铺事件(order.*、product.*、customer.*、cart.*、店铺侧的shipment.*)需要订阅:可通过 Partners MCPsalla_events action=subscribe(应用安装到店铺时的事件),或通过 Admin APIPOST /webhooks/subscribe并提供商家令牌(需要webhooks.read_writescope)。 - 使用什么技术栈? 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),而不要依赖此包。
- 注意 — 仅支持 Express。 该包使用 CommonJS,并通过文件系统动态调用
- 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.* 等)。
-
设置接收端:
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)。 - 每次都要明确设置策略。省略时会被视为
-
仅订阅店铺事件(如果应用只处理应用事件,可跳过此步骤):使用
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" }]
}
| 字段 | 类型 | 说明 |
|---|---|---|
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 |
- 注册会返回
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_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 规则”):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 维护 — 应以文档为准,而不要依赖本技能中固定的列表(事件名称和结构会变化)。可通过两种方式查询名称:
-
实时、以编程方式查询:
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 日志中检查投递尝试和响应代码
使用 webhook.site 测试: 复制一个唯一 URL → 将其设为 Webhook URL → 从演示商店触发事件 → 检查 webhook.site。若出现负载 → 问题出在你的服务器;若没有任何内容 → 问题出在订阅或范围。在同一页面添加自定义标头,以测试标头投递。
Webhook 活动日志: 合作伙伴 Portal → 你的应用 → Webhooks 日志会显示每次投递尝试、HTTP 代码和完整负载 — 调试服务器前先在此处检查。
关键资源
| 资源 | URL |
|---|---|
| Webhooks 文档 | https://docs.salla.dev/421119m0.md |
| 条件 Webhooks | 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 |
| Partner Portal | https://salla.partners |
| Telegram 社区 | https://t.me/salladev |