PAYATHON 2026

如何使用 Openpay subscription update API 更改套餐

支付小周

结论

Openpay 的 subscription update API 不支持直接修改已有订阅的 plan_id,因此不能把同一个 subscription 从 monthly plan 原地切换到 yearly plan。

需要按以下流程处理:

  1. 结束旧订阅,或将其设置为在当前周期结束时取消。
  2. 使用 yearly plan 创建新的 subscription。
  3. 根据切换策略处理生效时间、重复扣款,以及未使用周期的退款或抵扣。

新订阅会生成新的 subscription_id,旧订阅的付款记录仍保留在原订阅下。

为什么不能直接更新

订阅创建后,plan_id 决定扣款金额、扣款周期和重试规则等核心属性。Openpay 的更新接口只能修改其明确支持的订阅属性,不能假定以下请求可以更换套餐:

{
  "plan_id": "yearly-plan-id"
}

提交该字段后,接口可能忽略它,也可能返回参数错误。具体响应取决于使用的 Openpay API 版本,请以当前账户对应的官方 API 文档为准。

在当前周期结束后切换

升级或降级时,安排在当前周期结束后切换通常更稳妥,也能避免同一用户在一个周期内被重复扣款。

先将 monthly subscription 设置为在周期结束时取消:

curl --request PUT \
  --user "${OPENPAY_PRIVATE_KEY}:" \
  --header "Content-Type: application/json" \
  --data '{
    "cancel_at_period_end": true
  }' \
  "https://api.openpay.mx/v1/${MERCHANT_ID}/customers/${CUSTOMER_ID}/subscriptions/${SUBSCRIPTION_ID}"

业务系统还需要记录待切换的目标套餐:

{
  "customer_id": "customer-id",
  "current_subscription_id": "monthly-subscription-id",
  "next_plan_id": "yearly-plan-id",
  "switch_at": "current-period-end"
}

确认旧订阅结束后,再创建 yearly subscription:

curl --request POST \
  --user "${OPENPAY_PRIVATE_KEY}:" \
  --header "Content-Type: application/json" \
  --data '{
    "plan_id": "yearly-plan-id",
    "source_id": "source-or-card-id"
  }' \
  "https://api.openpay.mx/v1/${MERCHANT_ID}/customers/${CUSTOMER_ID}/subscriptions"

是否必须提供 source_id,取决于客户是否已有可复用的支付方式,以及当前 Openpay 接口对此场景的要求。

创建成功后,保存接口返回的新 subscription_id:

{
  "old_subscription_id": "monthly-subscription-id",
  "new_subscription_id": "yearly-subscription-id",
  "change_type": "upgrade"
}

立即切换套餐

如果 yearly plan 必须立即生效,可以先取消旧订阅,再马上创建新订阅:

curl --request DELETE \
  --user "${OPENPAY_PRIVATE_KEY}:" \
  "https://api.openpay.mx/v1/${MERCHANT_ID}/customers/${CUSTOMER_ID}/subscriptions/${SUBSCRIPTION_ID}"

随后创建 yearly subscription:

curl --request POST \
  --user "${OPENPAY_PRIVATE_KEY}:" \
  --header "Content-Type: application/json" \
  --data '{
    "plan_id": "yearly-plan-id",
    "source_id": "source-or-card-id"
  }' \
  "https://api.openpay.mx/v1/${MERCHANT_ID}/customers/${CUSTOMER_ID}/subscriptions"

这样处理可能导致用户刚支付 monthly 费用,又立即被收取 yearly 费用。monthly plan 未使用部分是退款还是折算为余额,需要由业务系统计算并执行,不能默认 Openpay 会自动按比例结算。

后端实现示例

下面的 Node.js 示例演示了在当前周期结束后切换套餐的基本流程。字段和错误结构需要根据实际使用的 Openpay SDK 或 HTTP 客户端进行调整。

async function schedulePlanChange({
  merchantId,
  customerId,
  subscriptionId,
  newPlanId,
  privateKey
}) {
  const auth = Buffer.from(`${privateKey}:`).toString("base64");
  const baseUrl = `https://api.openpay.mx/v1/${merchantId}`;

  const response = await fetch(
    `${baseUrl}/customers/${customerId}/subscriptions/${subscriptionId}`,
    {
      method: "PUT",
      headers: {
        Authorization: `Basic ${auth}`,
        "Content-Type": "application/json"
      },
      body: JSON.stringify({
        cancel_at_period_end: true
      })
    }
  );

  if (!response.ok) {
    const error = await response.text();
    throw new Error(`Unable to update subscription: ${error}`);
  }

  const subscription = await response.json();

  // 将目标套餐和计划切换时间保存到业务数据库。
  await savePendingPlanChange({
    customerId,
    oldSubscriptionId: subscriptionId,
    newPlanId,
    switchAt: subscription.current_period_end
  });

  return subscription;
}

确认旧订阅结束后,创建新订阅:

async function createReplacementSubscription({
  merchantId,
  customerId,
  planId,
  sourceId,
  privateKey
}) {
  const auth = Buffer.from(`${privateKey}:`).toString("base64");

  const response = await fetch(
    `https://api.openpay.mx/v1/${merchantId}/customers/${customerId}/subscriptions`,
    {
      method: "POST",
      headers: {
        Authorization: `Basic ${auth}`,
        "Content-Type": "application/json"
      },
      body: JSON.stringify({
        plan_id: planId,
        source_id: sourceId
      })
    }
  );

  if (!response.ok) {
    const error = await response.text();
    throw new Error(`Unable to create subscription: ${error}`);
  }

  return response.json();
}

注意事项

  • 需要 private key 的 Openpay API 不能在前端调用,套餐切换必须由服务端执行。
  • monthly 和 yearly 应使用两个独立的 plan,不能通过更新订阅来修改原 plan 的周期或金额。
  • 创建新订阅前,要确认旧订阅已经取消或处于预期状态,以免重复扣款。
  • 升级和降级可以采用不同策略。升级通常可以立即生效,降级通常安排在当前周期结束后生效。
  • 不能只根据前端回调判断订阅状态。应结合 Openpay 通知事件或服务端查询结果更新本地状态。
  • Webhook 可能重复投递,因此创建新订阅的处理逻辑必须具备幂等性。
  • 不要覆盖旧的 subscription_id。应保留新旧订阅之间的关联,便于退款、对账和审计。
  • cancel_at_period_end、周期结束时间字段和支付方式复用规则可能因 API 版本或地区产品而异。接入前应核对当前 Openpay 文档,并确认测试环境的实际响应。

备注:内容仅供参考。