关于 portaly-product
Portaly 数字产品集成 Skill,帮助开发者把自有网站连接到创作者的 Portaly 商品销售流程。网站负责商品展示,Portaly 托管结账、支付和购买后页面,并通过签名 Webhook 通知订单事件。
主要功能
- 01通过 API 获取商品信息
- 02创建托管结账会话
- 03重定向买家至 checkoutUrl
- 04处理签名 Webhook 事件
- 05支持测试模式与实时模式
适用场景
- 01在自有网站展示 Portaly 商品
- 02将买家引导至托管结账页面
- 03接收购买、支付失败和退款通知
- 04用测试密钥验证结账集成
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ portaly-ai/ portaly-skills --skill portaly-product安装后在 Claude Code 中输入/portaly-product确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/api-contract.mdbundle-pricing.mdcallback-signature-v1-vectors.jsoncallback-signature-v1.mdscripts/check_callback_vectors.mjsgo.modsign_callback.mjssign_callback.pysign_callback.webcrypto.mjsverify_callback_test.goverify_callback.go详细介绍
Portaly 数字产品集成
使用此 skill 帮助用户将自己的网站(通常使用 Cursor / v0 / Lovable / 等工具通过 vibe coding 搭建)接入创作者的 Portaly 数字产品进行销售。用户负责产品展示界面;Portaly 负责结账、支付、邮件和订单成功页面。
流程采用基于重定向的托管结账:
- 用户的网站通过 API 获取创作者的产品,并按照自己的方式展示。
- 买家准备付款时,用户的网站通过 API 创建结账会话,并将买家重定向到返回的
checkoutUrl。 - Portaly 处理支付、向买家发送确认邮件,并提供购后交付页面。
- 购买完成、支付尝试失败以及创作者之后退款时,Portaly 会向用户的回调 URL 发送带签名的 webhook。
Portaly 数字产品环境
API 主机(可通过 PORTALY_API_HOST 覆盖):
https://portaly.ai
| 方面 | 线上模式 | 测试模式 |
|---|---|---|
| API 密钥前缀 | pcs_live_ | pcs_test_ |
| 真实扣款 | 是 | 否——仅测试交易 |
| 结账时需输入的银行卡 | 买家的真实银行卡 | 结账页面显示的测试卡 |
| 已完成订单的归属 | 创作者的真实订单、收入和提现 | 独立的沙盒账本——不参与结算链路,但会列在 Payment 管理页面中(订单表已设为 Test) |
此 API 密钥与 portaly-payment skill(创作者订阅)共用。一个密钥,两个产品。测试密钥可在不产生真实扣款的情况下运行完整结账流程——使用测试密钥进行开发,生产环境再切换到线上密钥;但请阅读下方的测试模式,了解测试购买不会执行哪些操作。
支付完全由 Portaly 的托管结账页面处理——你不会看到或选择向买家收费的方式。无论如何,你的集成流程都相同:列出产品、创建会话、重定向并处理 webhooks。
测试模式
绝不要编造银行卡号。 测试模式结账页面会在银行卡字段正下方的醒目框中显示可用的测试卡;请告诉用户直接查看页面。此技能特意不提供银行卡号,因为页面才是唯一可信来源。
这张卡只能用于此技能创建的 checkoutUrl。 测试模式是结账会话的属性,而非产品的属性。同一产品也会在创作者自己的 Portaly 页面(portaly.cc/<slug>)上销售,而该页面的结账始终使用真实货币,不支持测试模式,也不会提示使用测试卡;在那里使用测试卡会被拒绝,并返回原始网关错误(例如 Failured3DS)。如果用户反馈“我测试了一下,结果遇到了奇怪的错误”,请先询问他们是在哪个页面付款的。
测试模式下的购买无法演练扣款后的流程。 扣款确实会执行(使用 TapPay 沙盒,因此不会有资金变动),Webhook 也确实会触发,但订单会写入单独的沙盒账本,且所有后续步骤都会跳过。请在创作者测试前告知他们,免得他们去寻找本就不该出现的内容:
- 买家不会收到确认邮件。 Portaly 的订单邮件只会针对正式订单集合触发,因此不会发送确认邮件。请检查你自己的处理程序输出,而不是查看收件箱。如果结账要求买家验证邮箱,买家仍会在测试模式下收到邮箱验证码;但收到这封邮件并不能证明他们也会收到确认邮件。
- 回调中的
orderSuccessPageUrl不会显示订单。 该页面只会在正式订单集合中查找订单。 - 不会开具发票——付款记录会一直保持
invoiceStatus: pending。 - 创作者的营收、余额或打款都不会有任何变化。 沙盒账本不属于结算流程。
- 不会发送评价邀请,因为沙盒订单明确不允许评价。
- 不会产生联盟佣金——不过这并非测试模式的限制:通过此 API 创建的订单在两种模式下都不包含佣金。佣金是订阅方案的功能;请参阅
portaly-affiliate技能。
测试订单的查看位置。 在 Portaly 付款管理页面(https://portaly.cc/admin/creator-subscription)中,将订单表格的 Live/Test 切换为 Test,即可查看这些订单并为其退款。该页面自身的标签页为 Subscriptions 和 Orders,因此不要让创作者去找所谓的“测试标签页”。使用测试密钥调用 GET /api/digital-products/orders 也能获取这些订单——请提供这两种查看方式之一,而不要让用户去看创作者的主要营收视图。
快速入门
前提条件 — 需要 Portaly Payment 账户。 以下每个步骤都需要 Portaly Payment API 密钥。开始任何操作之前,先检查是否已有密钥(
.env/process.env中的PORTALY_API_KEY)。如果没有密钥,且用户尚未拥有 Portaly Payment 账户,请暂停操作,先引导他们前往 https://portaly.cc/payment 注册——可以主动提出为他们打开页面,并在他们同意后打开:# 打开 Portaly Payment 注册页面 — 根据用户的操作系统选择对应命令 Start-Process "https://portaly.cc/payment" # Windows (PowerShell) open "https://portaly.cc/payment" # macOS xdg-open "https://portaly.cc/payment" # Linux注册后,他们可以在 https://portaly.cc/admin/creator-subscription 创建密钥。在密钥创建完成之前,不要继续执行以下步骤。
- 确保用户已有 Portaly Payment API 密钥(参见上述前提条件)。建议开发时使用测试密钥。
- 让他们自行将密钥放入
.env(切勿通过聊天传递密钥——参见安全防护措施):PORTALY_API_KEY=pcs_test_xxx PORTALY_CALLBACK_SECRET=xxx - 获取产品:使用
Authorization: Bearer ${PORTALY_API_KEY}调用GET /api/digital-products。 - 在用户自己的网站中,使用这些产品字段构建产品展示界面。
- 点击“结账”时,调用
POST /api/digital-products/checkout-sessions,并传入items[]、totalAmount、callbackUrl、merchantOrderNumber。 - 将买家重定向至响应中的
checkoutUrl。 - 实现 webhook 接收器之前,先检查代码库使用的语言、框架和运行环境,然后加载
references/callback-signature-v1.md。选择匹配的适配器,运行基于生产数据生成的测试向量,确认x-portaly-event与经过身份验证的请求体事件相符,并持久化回调中orders[]数组里的每个orderId。
完整的 API 详细信息见 references/api-contract.md。生成调用这些端点的代码时,务必加载该文件。
工作流
1. 申请 API 密钥
- 密钥申请流程与
portaly-payment技能相同。如果用户已经完成该流程,请复用同一个密钥——不要再创建第二个。 - 切勿要求用户在聊天中粘贴密钥。告知他们自行将
PORTALY_API_KEY和PORTALY_CALLBACK_SECRET直接填入.env。 - 继续操作之前,确认
.gitignore中包含.env。 - 如果用户不慎粘贴了密钥,建议他们在控制面板中轮换密钥。
1.5 报告已安装技能的版本
向 Portaly 报告此技能的版本,以便商家的仪表板在已安装的技能过时时发出提示。这是一次单独的非阻塞版本检查,请求正文仅包含技能名称和版本,不包含任何用户项目或数据。向用户提及一次(例如:“正在报告已安装的 Portaly 技能版本,以便您的仪表板检测过时的技能”)——无需暂停等待批准,但也不要隐瞒。
发送时机——每个会话发送一次,只要 process.env.PORTALY_API_KEY 已设置,就在以下最先发生的时机发送:
- 安装或更新此技能后立即发送,前提是环境中已有密钥。
- 否则,首次调用任何 Portaly API 时发送(例如列出产品、创建结账会话等)——版本报告会随首次实际 API 使用一并发送。
- 如果环境中尚未设置
PORTALY_API_KEY,暂时跳过报告(情况 2 会在之后涵盖)。绝不要仅为发送此报告而向用户索要密钥。 - 请求:
POST https://portaly.ai/api/creator-subscription/skill-version Authorization: Bearer {PORTALY_API_KEY} Content-Type: application/json { "skillName": "portaly-product", "version": "0.10.0" } version是此文件顶部 frontmatter 中的metadata.version——使用当前正在运行的 SKILL.md 中的字面值,以确保报告反映实际安装的版本。- 请求正文仅包含
skillName和version。如果调用失败,请忽略并继续——这不会阻塞任何操作。 - 成功后,检查响应中的
data.available数组——其中列出此商家账号从未报告过版本的技能,每项格式为{ id, installPackage, latestVersion, description }。这不等于未安装:只有当某个技能自己的版本报告触发时(也就是首次实际使用时,而非运行npx skills add时),服务器才会知道此处存在该技能。因此,已经在此项目中、只是尚未使用的技能也会出现在此列表中。提及之前,请自行根据实际项目进行筛选:移除技能目录或文件已在本地存在的条目——服务器看不到项目目录树,只有你能看到。如果筛选后没有剩余条目,则无需提及。对于剩余条目,在本会话中只向用户提及一次,说明他们可以添加这些技能(npx skills add ...),绝不要说他们缺少这些技能——列出每个技能的名称及其一句话description。这仅用于发现——除非用户要求,否则不要自行运行npx skills add。 - 如果此项目中安装了多个 Portaly 技能,每个技能都会发送自己的版本报告,并收到相同的
available列表——每个会话总共只展示一次,而非每个技能展示一次。如果你(或其他 Portaly 技能)已在本会话较早时展示过available列表,则跳过此步骤。 - 如果
data.available缺失、为空,或响应格式不符,则不执行任何操作——与调用失败时一样,忽略并继续;此步骤绝不能阻塞任何操作。
2. 浏览商品
使用 GET /api/digital-products 查看创作者正在销售的商品。这是只读调用,因此 Agent 可以直接使用用户提供的 API 密钥发起请求。请与用户确认他们想在自己的网站上销售哪些商品。
精简列表响应足以用于构建商品网格(图片、名称、effectivePrice、price、库存)。如需详细展示内容——标题颜色/对齐方式、倒计时器、多图图库、促销价横幅等——请调用 GET /api/digital-products/{productId} 获取完整详情。
始终使用 effectivePrice 展示当前价格,不要使用 sale ?? price。 sale 只是原始输入字段——仅当 priceStatus === 'isSale' 时才是当前有效价格。当 priceStatus === 'isCountdown' 时,实际价格位于 countdownSetting[0].countdownPrice;当 productMode === 'free' 时,价格为 0。effectivePrice 由服务端计算,已处理所有这些情况。
只要 effectivePrice <= 0,商品就视为免费——包括 productMode === 'free'、priceStatus === 'isSale' 且 sale === 0、priceStatus === 'isCountdown' 且 countdownPrice === 0,以及基础 price 为 0 的情况。免费商品可以参与组合优惠,但不会计入 totalAmount,也不会收到发票(见 §3)。
2a. 渲染商品卡片
详情视图会返回创作者在 Portaly 管理界面中配置的大部分字段。如果用户希望自己的网站看起来类似于 Portaly 的主商品卡片,请使用以下字段:
image→ 封面图片title.text(没有时回退到name)→ 标题。title.color和title.align是创作者配置的样式。- 价格:
- 主价格 → 始终使用
effectivePrice(已考虑促销/倒计时/免费情况)。 - 如果
effectivePrice < price→ 在旁边显示带删除线的price,作为“原价”。 - 徽标(可选的用户体验设计):
priceStatus === 'isSale'→ 显示“促销”徽标;priceStatus === 'isCountdown'→ 显示由countdownSetting[]驱动的倒计时横幅;productMode === 'free'→ 显示“免费”标签。 - 不要自行使用
sale ?? price——这会错误处理倒计时/免费商品。创建结账会话时,totalAmount也一样:将effectivePrice相加(再应用任何组合优惠),不要使用sale ?? price。
- 主价格 → 始终使用
stock+isStock+isShowStock:- 如果
!isStock→ 不显示库存 - 如果
isStock && stock === 0→ 缺货;使用stockButtonName作为 CTA 标签 - 如果
isStock && isShowStock→ 显示剩余数量 stock是读取时的可用库存,不是预留库存。其他买家可能会在你付款前买走最后一件,因此只能将其用于展示,并在结账时继续处理400 OUT_OF_STOCK(§3)。
- 如果
buttonName→ CTA 按钮标签(缺失时默认为“购买”/“立即購買”)productImages→ 详情页的其他图库图片videoUrl/videoImage/videoText→ 可选的预览视频
买家在用户网站上看到的内容(商品展示、购物车界面、“结账”按钮)完全由用户负责——他们可以自行设计。
2b. 将商品下架
创作者通常会在 Portaly 控制面板中管理商品目录,但如果他们的网站需要独立的上下架开关,可通过 POST /api/digital-products/{productId}/status 并传入 {"isActive": false} 将商品下架(传入 true 可重新上架,省略该字段则切换当前状态)。此操作需要全权限密钥——仅用于集成的密钥会返回 403 KEY_SCOPE_FORBIDDEN,这是正确结果,不应尝试绕过。
停用商品会拒绝针对该商品的新结账,并将其从列表端点中隐藏;但不会影响已下单的订单,因此现有买家仍会收到他们购买的商品内容。不要用此功能来表示“售罄”——结账时已强制执行库存限制,库存有限的商品会在售罄后自动停止销售。
完整契约请参阅 references/api-contract.md。
3. 创建结账会话
买家准备付款时,用户的后端会调用 POST /api/digital-products/checkout-sessions。
关于 totalAmount,有两条规则:
- 套装中至少有一件付费商品(
effectivePrice > 0)→totalAmount必须> 0。 - 套装中的商品全部免费(所有商品都满足
effectivePrice <= 0)→totalAmount必须恰好为0。买家无需填写银行卡信息,直接完成结账;不会扣款,也不会生成发票。仍需验证邮箱。
不匹配时会返回 400 TOTAL_AMOUNT_INVALID。
const res = await fetch(`${HOST}/api/digital-products/checkout-sessions`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.PORTALY_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
items: cart.map(p => ({ productId: p.id })),
totalAmount: cart.totalPrice, // for bundle, your discounted price
currency: 'TWD',
// Only declare `emailVerified` for an email YOU actually verified — never for one
// the buyer just typed into a form. Declaring it removes the emailed code entirely.
...(user?.emailVerified
? {
customerEmail: user.email, // your verified email → buyer skips the code
customerName: user.displayName, // pre-fills the name field (buyer can still edit)
emailVerified: true,
}
: { customerEmail: form.email }), // optional, pre-fill only — buyer still gets the code
callbackUrl: 'https://your-site.com/webhooks/portaly',
successRedirectUrl: 'https://your-site.com/thanks',
cancelRedirectUrl: 'https://your-site.com/cart',
merchantOrderNumber: yourInternalId,
metadata: { tracking: JSON.stringify({ userId: '...', cartId: '...' }) }, // your own data goes under `tracking` as one string (≤ 500 chars) — the Python/Go v1 adapters fail closed on other keys outside the committed schema
}),
})
const { data } = await res.json()
// Redirect the buyer:
return Response.redirect(data.checkoutUrl, 303)
验证会在开始时进行:如果 productId 无效、未启用或缺货,你会收到包含特定 error.code 的 400 响应。只有买家尝试在托管页面付款后,才会出现实际支付失败(买家可以在那里重试,或者你也可以创建新会话)。
即使你的商品网格刚显示某件商品有货,也可能遇到 OUT_OF_STOCK——创建会话不会预留商品,最后一件商品可能在此期间被其他买家买走。请将其视为预期情况:刷新该商品,并告知买家它刚刚售罄,而不要将其显示为买家操作失败。
4. 让 Portaly 运行托管结账
请将托管页面视为黑盒。不要尝试在客户端收集银行卡令牌。
托管页面会显示创作者品牌信息(创作者姓名、头像、颜色),并附有小型的“由 Portaly 提供支持”标记。页面会显示每件商品的 listedPrice;当 listedPrice < originalPrice 时,原价会以删除线显示。当 totalAmount 低于所有 listedPrice 的总和时(例如,你应用了套装折扣),页面会显示小计、折扣金额和最终总额。
5. 处理 Webhook
POST https://your-site.com/webhooks/portaly
Content-Type: application/json
x-portaly-event: digital_product.checkout.completed
x-portaly-timestamp: 2026-05-19T12:50:00.000Z
x-portaly-signature: <hex>
检查代码仓库使用的技术栈,然后加载 references/callback-signature-v1.md,并使用对应的 Node、WebCrypto、Python 或 Go 适配器。发布前,请针对该运行时运行 scripts/check_callback_vectors.mjs。V1 会使用 PORTALY_CALLBACK_SECRET 验证 ${timestamp}.${stableJson(JSON.parse(wireBody))},而非原始 HTTP 请求体;随后要求 x-portaly-event 与经过身份验证的请求体事件相匹配。
自定义 metadata 键与回调验证: 创建会话时发送的 metadata 会被回显到经过签名的回调请求体中。V1 使用 JavaScript 的 localeCompare 对对象键排序,而 Python 和 Go 适配器无法对任意键复现这一行为,因此遇到提交架构之外的任何 metadata 键时,它们都会拒绝验证。请将自定义数据放在 tracking 键下,并以 JSON 字符串形式传递(metadata: { tracking: JSON.stringify({ userId, cartId }) }):tracking 已包含在测试向量中,可通过所有适配器验证;由于限制针对的是键而非值,一个键即可承载任意结构。该字符串计入每个 metadata 值的 500 个字符上限,因此请只包含你之后确实会读取的字段。对于其他任何自定义键,请使用 Node/WebCrypto 适配器进行验证。
持久化保存:
sessionId(与event组合,作为结账幂等键——见下文)orders[](每项包含orderId、productId、allocatedAmount、orderSuccessPageUrl)merchantOrderNumbermetadata
如果 x-portaly-timestamp 与当前时间相差超过 5 分钟(无论早于还是晚于),请拒绝回调;对称时间窗口可以容忍常见的时钟偏差,而拒绝任何未来时间戳会导致合法回调间歇性失败。 结账幂等请使用 event + sessionId,退款幂等请使用 event + orderId;不要对所有事件类型使用同一个仅基于会话的键。
Portaly 会自动向买家发送邮件——每件已订购商品各发送一封购买确认邮件,每封邮件都包含该商品交付内容的订单成功页面链接。购买 3 件商品的套装时,预计会收到 3 封单独的邮件(免费商品不会触发邮件)。你无需自行发送邮件,也不负责交付内容。测试模式下不会发送这些邮件——在告知任何人查看收件箱之前,请先阅读上文的测试模式。
6. 处理支付失败(Webhook)
当买家的支付尝试被拒绝时,Portaly 会发送:
x-portaly-event: digital_product.checkout.failed
负载包含 sessionId、profileId、merchantOrderNumber、mode、paymentProvider、totalAmount、currency、customerEmail、failureReason、failedAt、metadata。此时不存在订单,因此没有 orders[]——请使用 event + sessionId 作为幂等键。test 模式的会话也会触发此事件(请检查 mode),因此部署处理程序后,沙盒端点会立即开始收到此事件。
利用此事件跟进买家(发送重试链接、提醒邮件),而不是悄无声息地错失销售机会。Portaly 会采用指数退避策略重试失败的端点,最多重试 5 次。
买家只是离开而没有付款时,不会触发 Webhook——如何查找此类结账记录,请参见第 8 步。
7. 处理退款(Webhook)
当创作者在 Portaly 管理后台为订单退款时,你会收到:
x-portaly-event: digital_product.order.refunded
负载包含 orderId、sessionId、amount。退款事件按订单触发——如果一个包含 5 件商品的组合订单全额退款,你会收到 5 个单独的 order.refunded 事件。
利用此事件撤销你在系统中授予的任何权益(例如,移除用户访问权限、减少许可证数量)。
8. 可选:列出订单和未完成的结账记录
GET /api/digital-products/orders 会返回通过此 API 密钥创建的订单。可用于对账,或在用户的管理后台中显示“我的购买”面板。
GET /api/digital-products/checkout-sessions 会返回结账会话,包括最终未生成订单的会话——这是查看失败和放弃结账记录的唯一方式,因为 /orders 只包含成功购买的记录。使用 ?outcome=failed|abandoned|pending|completed 进行筛选,使用 ?limit=(1–200)和 ?startAfter=(取自 pagination.startAfter)进行分页。此接口的范围限定为密钥所属的账户和模式,而非密钥本身,因此轮换密钥不会导致历史记录不可见。
进行对账时,也要遍历未筛选的列表,而不只是使用 ?outcome=。 有两种状态不会出现在结果筛选中:网关回调丢失的进行中支付会显示为 abandoned,而已成功扣款但尚未完成处理的支付则不匹配任何 outcome(只会出现在未筛选的列表中)。这两类记录恰恰都需要人工检查。请参见 references/api-contract.md → 已知盲点。
首选响应格式
为用户实现时,请返回:
- 所需的确切
.env键 - 需要添加的后端端点(附可直接复制粘贴的代码)
- Webhook 处理程序代码(包含签名验证)
- 用户需要在自己一侧持久化的数据的最小架构(订单表)
- 简短的测试计划,并注明付款页面:“打开后端返回的
checkoutUrl,使用页面显示的测试卡付款;单件商品和组合商品各测试一次,然后在 Portaly 管理后台中切换订单表至 Test 并为其中一笔退款。”再添加一个失败分支步骤:结账页面不提供会被拒绝的银行卡,因此应使用scripts/sign_callback.mjs对该负载签名,并将其 POST 到你自己的 Webhook,以此测试digital_product.checkout.failed,而不是尝试让真实扣款失败。
安全防护
- 开发时默认使用测试模式。
pcs_live_密钥会创建真实且需要付费的结账会话。如果加载的密钥以pcs_live_开头,请先向用户确认是否确实要使用实时模式,再创建实时结账会话。绝不能在未告知用户的情况下让买家进入生产环境的计费流程。 - 测试模式属于结账会话,而非产品。 测试卡仅适用于使用
pcs_test_密钥创建的checkoutUrl。在创作者自己的 Portaly 页面购买同一产品始终会产生真实扣款,而测试卡在该页面会失败,并返回原始网关错误——绝不要告诉用户可以“直接去 Portaly 试试”来测试集成。 - 绝不要在聊天中回显机密。 请用户自行将
PORTALY_API_KEY和PORTALY_CALLBACK_SECRET写入.env。 - 在建议提交任何内容之前,务必确认
.gitignore包含.env。 - 处理 webhook 负载之前,务必验证 webhook 签名。 发往
/webhooks/portaly的不可信 POST 请求可能导致授权发放。 - 务必检查
x-portaly-timestamp是否在有效时间范围内(如果与当前时间相差超过 5 分钟,无论是过去还是未来,都应拒绝;对称时间窗口可容忍常见的时钟偏差)。 - 务必通过 HTTPS 提供
callbackUrl。 - 处理 webhook 时,使用
event + sessionId(结账完成/失败)和event + orderId(退款)作为幂等键——webhook 可能会重新投递。不要对所有事件类型共用仅包含会话的键。 - 不要将买家端的
successRedirectUrl视为付款凭证。 只有 webhook(或轮询会话)才能确认真实的completed状态。 - 不要将机密放入
metadata。 这些内容会在 webhook 和日志中回传。 - 套餐定价由你决定,但大幅低于创作者标价总额可能会蚕食创作者的主店铺收入。上线前请与创作者讨论。
交付内容
每次集成结束时,请向用户提供:
- 可正常工作的
GET /api/digital-products调用,并返回其商品清单 - 用于创建结账会话的后端端点
- 已验证签名处理逻辑的 webhook 接收端
- 确认用户已在测试模式下完成端到端测试(一次单品购买 + 一次套餐购买)
资源
references/api-contract.md— 完整的端点契约:请求/响应结构、错误代码、webhook 负载和订单文档字段。每当生成调用 API 的代码时,都要加载此文件。references/bundle-pricing.md— 按比例拆分的算法及示例。references/callback-signature-v1.md— 运行时路由、精确的 v1 契约、安全的处理程序顺序、失败关闭边界和诊断指南。references/callback-signature-v1-vectors.json— 使用已提交的生产契约生成的合成负载及签名;请使用这些数据,而非自行签名/自行验证的测试夹具。scripts/check_callback_vectors.mjs— 针对已提交的正向、负向和失败关闭用例,运行所选的 Node、WebCrypto、Python 或 Go 适配器。scripts/sign_callback.mjs— Node.js HMAC 验证参考代码(复制到用户的项目中)。scripts/sign_callback.webcrypto.mjs— 适用于无法导入node:crypto的边缘运行时(Cloudflare/Vercel Edge、Deno、InsForge 边缘函数)的 WebCrypto HMAC 验证参考代码。使用相同的方案和逐字节一致的stableJson,并通过crypto.subtle进行验证。scripts/sign_callback.py— 适用于已提交的回调密钥域的 Python 适配器;对任意 metadata 密钥和不受支持的数字采取失败关闭处理。scripts/verify_callback.go和scripts/verify_callback_test.go— Go 适配器,以及从生产实现派生的测试和失败关闭测试。