关于 paddle-sandbox-testing
paddle-sandbox-testing 是供开发者和 Agent 在上线前验证 Paddle 集成的技能,涵盖沙盒配置、测试支付、Webhook 模拟及环境差异,无需收取真实款项。
主要功能
- 01沙盒与生产环境配置指南
- 02测试卡支付场景
- 03单事件 Webhook 模拟
- 04订阅生命周期场景模拟
- 05通过公网隧道进行本地测试
适用场景
- 01上线前验证结账流程
- 02单独测试 Webhook 处理程序
- 03演练订阅续费与取消
- 04检查支付失败和催款处理
MCP 相关
当有 Paddle MCP 服务可用时,该技能说明如何通过其 search 和 execute 工具以编程方式创建模拟并运行。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ paddlehq/ paddle-agent-skills --skill paddle-sandbox-testing安装后在 Claude Code 中输入/paddle-sandbox-testing确认已加载。
详细介绍
在沙盒中测试 Paddle 集成
何时使用此技能
在正式上线前验证 Paddle 集成时,使用此技能——运行仿真的结账流程、触发 Webhook 事件,并测试订阅状态变更,而不会收取真实款项。涵盖沙盒配置、测试卡号、Webhook 模拟器(单个事件和场景)、通过公共隧道进行本地开发,以及容易导致生产问题的沙盒与线上行为差异。
此技能是其他所有操作的安全保障。每次发布时,都将它与 checkout-web、webhooks 和 subscription-sync 一起使用——沙盒测试是发现集成错误成本最低的方式。
沙盒与生产环境一览
Paddle 提供了两个完全独立的环境:
| 沙盒 | 生产环境 | |
|---|---|---|
| 用途 | 开发和测试 | 真实客户、真实资金 |
| API 基础 URL | sandbox-api.paddle.com | api.paddle.com |
| 控制面板 | sandbox-vendors.paddle.com | vendors.paddle.com |
| API 密钥 | 单独一套 | 单独一套 |
| 客户端令牌 | 单独一套 | 单独一套 |
| 通知目标 | 单独一套 | 单独一套 |
| 结账和账户设置 | 单独一套 | 单独一套 |
| 产品 / 价格 / 客户 | 相互独立(不会同步) | — |
| 接受的卡片 | 仅限测试卡(拒绝真实卡片) | 真实卡片 |
| 退款 | 自动批准(约 10 分钟) | 大多数由 Paddle 审核 |
| 税费 | 计算但不收取 | 按实际税费处理 |
| 域名审批 | 无需审批(任何域名均可) | 每个域名都需手动审批 |
两者之间不会共享任何内容。 沙盒中的 pri_01h... 在生产环境中并不存在。通知目标彼此独立,客户也彼此独立。这是有意为之——这样一来,“先在沙盒中测试,再切换到线上”就是默认的安全做法。
步骤 1:配置沙盒
在 sandbox-login.paddle.com/signup 注册沙盒账号(与生产环境的注册相互独立)。然后:
- 设置至少一个产品和一个价格——如果尚未完成,请参阅
catalog-setup。 - 前往 Paddle > My account > Settings > Authentication 并创建:
- 一个客户端令牌(在 Client-side tokens 标签页),用于
Paddle.js。 - 一个服务端 API 密钥(在 API keys 标签页),用于 Node SDK。为其授予本技能的 API 和 MCP 调用所需的权限:
notification_simulation.write和notification_simulation.read(模拟器)、notification.read和notification.write(通知日志和重放)、通过 API 创建客户端令牌时所需的client_token.write,以及用于检查和设置默认支付链接的checkout_settings.write。如需使用此密钥运行 Paddle CLI,请让用户自行登录沙盒:paddle login --environment sandbox --api-key …。
- 一个客户端令牌(在 Client-side tokens 标签页),用于
- 检查是否已设置沙盒默认支付链接——否则结账页面无法打开。如果有可用的 Paddle MCP 服务,请在
paddle-sandbox上调用client.settings.checkoutUrl.get()(GET /settings/checkout-url);default_url为null表示尚未设置。请使用用户自有域名下的页面,而非localhost。沙盒不需要已批准的域名。如果更新时返回checkout_url_domain_not_approved,请在 Paddle > My account > Settings > Website approval 下添加该域名;沙盒会立即批准,然后重试。请先向用户询问 URL,再通过client.settings.checkoutUrl.update({ default_url: "https://example.com/checkout" })设置。使用 Paddle CLI 时,请运行paddle --environment sandbox settings checkout-url get和… settings checkout-url update --json '{"default_url": "…"}'。也可以让用户在 Paddle > Checkout > Checkout configuration 下设置。 - 记下 ID 并在本地环境中设置:
# .env.local — sandbox values
NEXT_PUBLIC_PADDLE_ENV=sandbox
NEXT_PUBLIC_PADDLE_CLIENT_TOKEN=test_...
PADDLE_API_KEY=pdl_sdbx_apikey_...
PADDLE_NOTIFICATION_WEBHOOK_SECRET=pdl_ntfset_...
沙盒的 PADDLE_API_KEY 前缀为 pdl_sdbx_apikey_,生产环境密钥的前缀为 pdl_live_apikey_。不确定当前连接的是哪个环境时,随时查看前缀。
步骤 2:使用测试卡
在沙盒中,只能使用测试卡。真实银行卡会被拒绝。标准测试卡:
Card number: 4242 4242 4242 4242
Expiry: any future month/year
CVC: any 3 digits
ZIP/Postal: any value
Name: any value
其他测试卡会触发特定场景:
| 卡号 | 结果 |
|---|---|
4242 4242 4242 4242 | 有效卡,无需 3DS |
4000 0038 0000 0446 | 有效卡,需进行 3DS |
4000 0566 5566 5556 | 有效 Visa 借记卡 |
4000 0000 0000 0002 | 被拒绝的卡 |
4000 0027 6000 3184 | 初次支付成功,后续续订失败(催收流程) |
完整列表见 developer.paddle.com/sdks/sandbox#test-cards。使用失败测试卡,确认 checkout.payment.failed 处理逻辑(每次支付尝试失败时都会触发)、催收流程和 subscription.past_due 处理逻辑端到端正常运行。checkout.payment.error 则有所不同:它表示没有可用的支付方式。
步骤 3:Webhook 模拟器
模拟器会向其中一个通知目标发送伪造但签名有效的 webhook 事件,无需经过真实流程。模拟事件不会更改账号中的任何真实数据。提供两种模式:
单个事件。 选择一种事件类型(例如 subscription.canceled)——模拟的 type 就是事件名称。编辑 JSON 负载,或让 Paddle 填入示例数据,然后运行。Paddle 会使用目标的密钥对其签名,并将其 POST 到目标 URL。可用此功能隔离测试特定处理逻辑——例如:“收到 subscription.paused 时,我的 UI 会怎样显示?”
场景。 一系列预定义事件,模拟真实流程:
场景 type | 事件(默认选项) |
|---|---|
subscription_creation | transaction.created、customer.created、address.created、transaction.ready、transaction.paid、subscription.created、subscription.activated(或 subscription.trialing)、transaction.completed、payment_method.saved — 各步骤之间会有 transaction.updated |
subscription_renewal | subscription.updated、transaction.created、transaction.billed、transaction.updated、transaction.paid、transaction.updated、transaction.completed |
subscription_pause | subscription.updated、subscription.paused |
subscription_resume | subscription.updated、subscription.resumed、transaction.created、transaction.billed,然后是与续订相同的付款事件 |
subscription_cancellation | subscription.updated、subscription.canceled |
选项会改变事件序列。暂停和取消的 effective_from 默认值为 "immediately";设为 "next_billing_period" 时,会先添加一个带有 scheduled_change 的 subscription.updated。续订和恢复接受 payment_outcome — "failed" 会将付款事件替换为 transaction.payment_failed、transaction.past_due 和 subscription.past_due。添加真实的沙盒实体(例如 subscription_id),即可在负载中填入其详细信息;实体本身不会被更改,但 API 密钥需要具有读取权限(例如 subscription.read)。
前往 Paddle > Events > Simulations 查找模拟器。如果你可以使用 Paddle MCP 服务,则可以在一次 execute 调用中以编程方式驱动模拟器,而无需使用控制面板 — 先创建模拟,然后针对它创建一次运行:
async (client) => {
const sim = await client.simulations.create({
name: "Subscription created flow",
type: "subscription_creation",
// Required: the destination that receives the simulated events.
notification_setting_id: "ntfset_...",
});
const run = await client.simulations.runs.create(sim.id);
return { simulation_id: sim.id, run_id: run.id };
}
请注意,client.simulations.runs.create 嵌套在 simulations 下(而不是顶层的 client.simulationRuns),且模拟 ID 是位置路径参数。场景选项放在 config 中,并以场景类型为键 — 例如:config: { subscription_cancellation: { entities: { subscription_id: "sub_..." }, options: { effective_from: "next_billing_period" } } }。
Paddle MCP 为每个服务提供三个工具(
search、execute、report_missing_tool)。工作流:调用search确认准确的方法名称和参数结构,然后调用execute,传入一个会调用client.<resource>.<operation>(...)的异步函数。方法路径使用 camelCase(client.clientTokens.create、client.pricingPreview.preview)。请求体参数和响应字段使用 snake_case(tax_category、product_id、unit_price、currency_code)。列表方法返回{ pagination: { hasMore, estimatedTotal }, <resource>: [...] },而不是data。数组的键是资源的 API 路径片段,因此方法路径使用 camelCase 时,该键使用 kebab-case:(await client.products.list()).products、(await client.clientTokens.list())["client-tokens"]。当pagination.hasMore为 true 时,使用{ after: "<last_id>" }翻页,不要使用.next()。未分页的列表(例如client.currencies.list())会返回普通数组。单实体方法会返回该实体。在一次execute中串联多步工作流;变量不会在不同调用之间保留。硬性限制:每次 execute 最多 50 次 API 调用、超时 30 秒,代码最多 32,000 个字符。
Paddle CLI(
paddle,v0.2.0 或更高版本)可使用用户保存的登录凭据,从终端执行 Paddle API 操作。资源命令与 API 参考一致:paddle products create即为“创建产品”。运行paddle <resource> --help查找命令;在 create 或 update 命令中添加--schema可查看请求体;对于没有对应命令的端点,可使用paddle api <path>。它默认连接实时账户,因此除非用户正在使用实时环境,否则请传入--environment sandbox。Agent 运行该命令时,它会返回 API 的 JSON(data、meta),其中meta.environment会标明所用账户。取消和删除命令需要--confirm <id>。只有在用户已同意执行特定取消或删除操作后才能添加此参数,绝不能用它来规避用户的拒绝。paddle api的写入操作完全不会经过检查,因此也应同样对待。退出代码5表示403:权限缺失,或登录的是另一个账户。退出代码7表示验证错误。如果paddle whoami显示未登录,请让用户自行运行paddle login(sandbox 需要 API 密钥:paddle login --environment sandbox --api-key …),绝不要在聊天中索要密钥。使用 OAuth 登录的实时账户可以执行用户 Paddle 角色允许的任何更改,因此请逐项说明每项实时更改,并在执行前等待用户确认。对于 CI/CD,请将仅具备所需权限的实时 API 密钥设置为PADDLE_API_KEY。安装方法请参阅 Paddle CLI。
使用 Paddle CLI 时,请将相同步骤作为独立命令运行。请求体使用相同的 snake_case JSON:
paddle --environment sandbox simulations create --jq '.data.id' --json '{
"name": "Subscription created flow",
"type": "subscription_creation",
"notification_setting_id": "ntfset_..."
}'
paddle --environment sandbox simulation-runs create ntfsim_...
使用模拟器:
- 在控制面板中,前往 Paddle > Events > Simulations。
- 选择一个场景或单个事件。
- 选择要发送到的通知目标。你需要选择已有目标,而不是输入 URL,因此请先将该目标指向通过隧道访问的本地 URL 或已部署的预览 URL。
- (可选)对于单个事件,编辑负载 JSON,使用特定 ID(例如你已在数据库中创建的
customer_id)。对于场景,请添加真实的 sandbox 实体。 - (可选)配置模拟(例如,将付款结果设为
failed,以模拟付款失败)。 - 运行模拟,并查看本地服务日志。
目标的 Usage type 必须为“Simulation”或“Both”(traffic_source: "simulation" 或 "all")——不能使用“Platform”目标。此外,目标必须处于启用状态,并已订阅模拟中的每种事件类型;任何无法接收的事件都会标记为 aborted,且不会发送。“Simulation”目标不会收到真实事件,因此,对于也用于真实结账的本地目标,请使用“Both”。
每个模拟事件只会尝试投递一次,不会重试。在 Paddle > Events > Simulations 中查看该运行的结果,或使用 client.simulations.runsEvents.list(simulationId, runId)——每个事件都有一个 status(success、failed、aborted、pending),以及请求和响应。使用 client.simulations.runsEvents.replay(simulationId, runId, eventId) 重新发送单个事件。使用 CLI 时,执行 paddle simulation-run-events list <simulation_id> <run_id> 和 paddle simulation-run-events replay <simulation_id> <run_id> <event_id>。
模拟事件上的签名使用目标的密钥,因此你的常规验证代码无需更改即可正常工作。如果验证失败,说明你在 PADDLE_NOTIFICATION_WEBHOOK_SECRET 中设置的密钥与当前模拟所针对的目标不匹配。
在 CI 中运行模拟
若要在每次变更时检查已部署的预览环境,可通过 Paddle CLI 在 CI 中运行模拟。创建一次模拟,并将其指向用于预览 URL 的目标,然后在每次构建中启动一次新运行,并在有任何事件未能投递时使构建失败。将具有模拟器所需权限的沙盒 API 密钥存储为 CI 密钥,切勿提交该密钥:
# CI secrets and variables: PADDLE_API_KEY (sandbox key), PADDLE_SIMULATION_ID (ntfsim_...)
export PADDLE_ENVIRONMENT=sandbox
RUN_ID=$(paddle simulation-runs create "$PADDLE_SIMULATION_ID" --jq '.data.id')
while [ "$(paddle simulation-runs get "$PADDLE_SIMULATION_ID" "$RUN_ID" --jq '.data.status')" = pending ]; do
sleep 5
done
FAILED=$(paddle simulation-run-events list "$PADDLE_SIMULATION_ID" "$RUN_ID" \
--jq '[.data[] | select(.status != "success")] | length')
[ "$FAILED" = 0 ] || { echo "$FAILED simulated events weren't delivered"; exit 1; }
步骤 4:通过隧道为 localhost 接收真实事件
模拟器可以覆盖大多数测试场景。对于真实流程(例如完成一次实际的沙盒结账,以验证完整链路),Paddle 需要能够访问你的本地计算机。请使用隧道:
Hookdeck(推荐):
# Install: brew install hookdeck
hookdeck listen 3000 paddle --path /api/webhook
# Copy the source URL it prints ("Requests to → https://...")
paddle 是 Hookdeck 源名称,--path 是你的 Webhook 路由。将沙盒通知目标指向源 URL,并将 Usage type 设为 “Both”,以接收真实事件和模拟事件。重启 CLI 后源 URL 保持不变,因此只需设置一次。Hookdeck 会在不同会话之间保留事件历史记录,因此,如果 CLI 停止期间有事件到达,可从 Hookdeck 或 Paddle 重新发送。
调试投递。 对于真实事件,可在 Paddle > Events > Notifications 中打开通知,或使用 client.notifications.logs.list("ntf_...") 查看每次投递尝试,并使用 client.notifications.replay("ntf_...") 重新发送。对于模拟事件,请查看模拟运行中的事件(见步骤 3)。
步骤 5:典型的端到端测试
以下是验证全新集成的流程:
- 建立隧道 localhost:
hookdeck listen 3000 paddle --path /api/webhook。 - 将沙盒目标指向隧道的源 URL(只需设置一次——重启后不会改变)。
- 打开定价页面 → 点击 Subscribe。
- 付款时使用
4242 4242 4242 4242。 - 确认:
- 浏览器跳转到你的
successUrl。 - 服务器日志显示所有事件
- 数据库中有新的客户和订阅记录(如果你已接入
subscription-sync)。 - 控制面板在 Transactions 下显示该交易。
- 浏览器跳转到你的
- 取消订阅,以下两种方式任选其一:
- **真实取消:**使用 API 或客户门户取消刚创建的沙盒订阅(见
subscription-cancel)。 - **模拟取消:**运行
subscription_cancellation模拟,在entities.subscription_id中填写该订阅的 ID,并设置effective_from: "next_billing_period"。这只会发送载荷使用该订阅详情的事件——真实订阅仍会保持有效,因此之后你的数据库与 Paddle 中的状态会不一致。
- **真实取消:**使用 API 或客户门户取消刚创建的沙盒订阅(见
- 确认:
- 真实取消,
effective_from: "next_billing_period":收到一个subscription.updated事件,其中scheduled_change.action: "cancel",且状态仍为active。只有在计费周期结束时才会收到subscription.canceled。 - 真实取消,
effective_from: "immediately":收到一个状态为canceled的subscription.canceled事件;你的记录应更新为canceled。 - 模拟取消:同一次运行中,先收到一个带有
scheduled_change的subscription.updated,然后收到subscription.updated和subscription.canceled,两者状态均为canceled。
- 真实取消,
沙盒与实时环境:可能让你措手不及的差异
这些行为在沙盒环境和实时环境中有所不同。
| 行为 | 沙盒 | 正式环境 | 重要性 |
|---|---|---|---|
| 调整(退款/抵扣金) | 退款会创建为 pending_approval,并在约 10 分钟内自动批准。抵扣金会立即变为 approved。 | 大多数退款会保持 pending_approval,直到 Paddle 审核。抵扣金会立即变为 approved。 | 不要在 adjustment.created 时就将退款视为完成——应处理 adjustment.updated,其中 status 会从 pending_approval 变为 approved(或 rejected)。 |
| Webhook 重试 | 总共尝试 3 次,均在约两分钟内完成 | 总共最多尝试 60 次,持续约两天 | 沙盒处理程序失败后,约两分钟后就不会再有重试——修复后重放该事件。不要编写依赖精确时序的测试。 |
| 付款 | 无付款 | 向你的银行账户转入真实资金 | 仅针对模拟付款测试付款 Webhook 处理。 |
| 域名审批 | Checkout 可在任何页面上打开,包括 localhost,且添加域名后会立即获批。默认付款链接不需要已获批的域名,但请使用自有域名下的页面,而非 localhost | 在 Paddle > My account > Settings > Website approval 中审批每个域名。你的默认付款链接(Paddle > Checkout > Checkout configuration)必须位于已获批的域名上。 | 忘记审批域名会导致正式环境的 Checkout 无法加载。使用 GET /checkout-domains 检查每个域名的 status。 |
| Checkout“测试模式”标记 | 在结账页面上显示 | 不显示 | 快速直观地检查结账页面使用的是哪个环境。 |
| 催缴(Retain) | 不可用 | 可用 | Retain 可处理取消或暂停逾期订阅。 |
| 邮件发送 | 只会发送到与你账户邮箱域名相同的地址。发送到其他地址的邮件会转发到你的账户邮箱。 | 发送到客户记录中的邮箱(真实邮箱) | 你会收到每位测试客户的邮件,因此请检查自己的收件箱。 |
常见陷阱
- 混用沙盒密钥和正式环境密钥。 务必确认
PADDLE_API_KEY和NEXT_PUBLIC_PADDLE_ENV对应正确的环境。 - 在生产环境中使用沙盒环境参数。 在生产环境中初始化 Paddle.js 时,务必确认环境参数已设为
production。 - 重启后隧道 URL 发生变化。 每次都会分配新 URL 的隧道(例如免费的 ngrok URL)需要在每次重启后更新目标 URL。症状:事件在 Paddle 一侧触发了,但始终没有到达你的代码。Hookdeck 源 URL 保持不变。
- 模拟事件标记为
aborted。 目标的 Usage 类型不是 “Simulation” 或 “Both”,处于非活动状态,或未订阅该事件类型。 - “只为确认一下”而使用真实银行卡测试。 沙盒会拒绝真实银行卡。生产环境会接受并扣款。
- 未测试失败路径。 使用
4000 0000 0000 0002确认checkout.payment.failed的处理逻辑,然后使用模拟器测试订阅续费失败(payment_outcome: "failed")。 - 在代码中为两个环境使用相同的通知目标密钥。 两者并不相同。每个环境中的每个目标都有自己的密钥。
- 只测试成功路径。 真实用户可能遇到各种异常状态,例如银行卡被拒、试用期到期、立即取消。使用模拟器触发每种边缘情况,并观察代码的响应。