POST /v1/actions/subscription-order/cancel-order
此端点也有 customer 侧的 session-token 调用流程:见 取消订阅(Customer)。
取消行为
| 当前状态 | 操作 | 结果状态 |
|---|---|---|
pending(新购) | 立即取消,不调用 PSP | canceled |
pending(换方案新订阅) | 立即取消,PSP 取消按发起时刻投递 | canceled |
active | 在周期结束时取消(通过 PSP) | canceling -> 周期结束时变为 canceled |
past_due | 立即取消(通过 PSP) | canceling -> PSP 确认后变为 canceled |
- pending:直接取消,状态变为
canceled,不经过canceling - pending 的换方案新订阅:同样直接变为
canceled,区别是那笔待生效订阅的 PSP 取消按发起时刻投递,因此它在原定切换时刻既不激活也不扣款。原订阅一格不动:保持canceling,不回到active - active:触发 PSP 取消(在计费周期结束时生效)。本地状态变为
canceling,然后在周期结束时通过 Webhook 更新为canceled - past_due:PSP 取消立即投递,不排到周期末。本地状态变为
canceling,PSP 确认后通过 Webhook 更新为canceled。该路径不发送「取消中,可用至 X 日」邮件 —— 计费周期已经结束
处于
canceling 的订阅通常可以通过恢复订阅撤回,但发起取消时存在未结清扣款的订阅不可撤回,该端点返回 400。取消 past_due 订阅必然属于此类。因已确认的换方案而处于
canceling 的订阅永远不可撤回,取消那笔待生效的新订阅、把变更作废之后也一样:支付渠道在确认变更时就已把原订阅置为待取消,且不提供恢复接口。它单向走向 canceled;想让买家继续付费,请重新下单。请求体
| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
orderId | string | 是 | 订阅订单 ID(Short ID 格式 ORD_xxx) |
请求示例
import { WaffoPancake } from "@waffo/pancake-ts";
const client = new WaffoPancake({
merchantId: process.env.WAFFO_MERCHANT_ID!,
privateKey: process.env.WAFFO_PRIVATE_KEY!,
});
const result = await client.orders.cancelSubscription({
orderId: "ORD_2aUyqjCzEIiEcYMKj7TZtw",
});
console.log(result.orderId); // "ORD_2aUyqjCzEIiEcYMKj7TZtw"
console.log(result.status); // "canceling" or "canceled"
// Assumes callApi() is defined as shown in the Authentication guide
const result = await callApi("POST", "/v1/actions/subscription-order/cancel-order", {
orderId: "ORD_2aUyqjCzEIiEcYMKj7TZtw",
});
// Assumes callApi() is defined as shown in the Authentication guide
String result = callApi("POST", "/v1/actions/subscription-order/cancel-order",
"{\"orderId\":\"ORD_2aUyqjCzEIiEcYMKj7TZtw\"}");
# Assumes call_api() is defined as shown in the Authentication guide
result = call_api("POST", "/v1/actions/subscription-order/cancel-order", {
"orderId": "ORD_2aUyqjCzEIiEcYMKj7TZtw",
})
// Assumes callAPI() is defined as shown in the Authentication guide
result, err := callAPI("POST", "/v1/actions/subscription-order/cancel-order",
`{"orderId":"ORD_2aUyqjCzEIiEcYMKj7TZtw"}`)
// Assumes call_api() is defined as shown in the Authentication guide
let result = call_api("POST", "/v1/actions/subscription-order/cancel-order",
r#"{"orderId":"ORD_2aUyqjCzEIiEcYMKj7TZtw"}"#
).await?;
// Assumes call_api() is defined as shown in the Authentication guide
call_api("/v1/actions/subscription-order/cancel-order",
"{\"orderId\":\"ORD_2aUyqjCzEIiEcYMKj7TZtw\"}");
// Assumes call_api() is defined as shown in the Authentication guide
auto result = call_api("/v1/actions/subscription-order/cancel-order",
R"({"orderId":"ORD_2aUyqjCzEIiEcYMKj7TZtw"})");
MERCHANT_ID="MER_2aUyqjCzEIiEcYMKj7TZtw"
TIMESTAMP=$(date +%s)
BODY='{"orderId":"ORD_2aUyqjCzEIiEcYMKj7TZtw"}'
BODY_HASH=$(echo -n "$BODY" | openssl dgst -sha256 -binary | base64 | tr -d '\n')
CANONICAL="POST
/v1/actions/subscription-order/cancel-order
$TIMESTAMP
$BODY_HASH"
SIGNATURE=$(echo -n "$CANONICAL" | openssl dgst -sha256 -sign private_key.pem | base64 | tr -d '\n')
curl -X POST "https://api.waffo.ai/v1/actions/subscription-order/cancel-order" \
-H "Content-Type: application/json" \
-H "X-Merchant-Id: $MERCHANT_ID" \
-H "X-Timestamp: $TIMESTAMP" \
-H "X-Signature: $SIGNATURE" \
-d "$BODY"
MERCHANT_ID="MER_2aUyqjCzEIiEcYMKj7TZtw"
TIMESTAMP=$(date +%s)
BODY='{"orderId":"ORD_2aUyqjCzEIiEcYMKj7TZtw"}'
BODY_HASH=$(echo -n "$BODY" | openssl dgst -sha256 -binary | base64 | tr -d '\n')
CANONICAL="POST
/v1/actions/subscription-order/cancel-order
$TIMESTAMP
$BODY_HASH"
SIGNATURE=$(echo -n "$CANONICAL" | openssl dgst -sha256 -sign private_key.pem | base64 | tr -d '\n')
wget -qO- --post-data="$BODY" \
--header="Content-Type: application/json" \
--header="X-Merchant-Id: $MERCHANT_ID" \
--header="X-Timestamp: $TIMESTAMP" \
--header="X-Signature: $SIGNATURE" \
"https://api.waffo.ai/v1/actions/subscription-order/cancel-order"
成功响应 (200) — 活跃订阅
{
"data": {
"orderId": "ORD_2aUyqjCzEIiEcYMKj7TZtw",
"status": "canceling"
}
}
成功响应 (200) — 待处理订阅
{
"data": {
"orderId": "ORD_2aUyqjCzEIiEcYMKj7TZtw",
"status": "canceled"
}
}
响应字段
| 字段 | 类型 | 说明 |
|---|---|---|
orderId | string | 订单 ID(Short ID) |
status | string | 新的订单状态(canceling 或 canceled) |
错误响应
重试策略:4xx 一律不要重试 — 修正请求后重发。5xx 指数退避重试(起步 5s,最多 3 次)。
| 状态码 | errors[0].message | 含义 | 推荐处理 |
|---|---|---|---|
| 400 | Missing X-Context-Merchant-Id header | API Key 认证下 merchant 上下文缺失 | 检查认证流水线 |
| 400 | Missing required field: orderId | orderId 缺失、为空或不是字符串 | 修正请求体后重新提交 |
| 400 | Invalid JSON body | 请求体不是合法 JSON | 修正请求体后重新提交 |
| 400 | Expected format: ORD_xxx, got "..." | orderId Short ID 解码失败 | 修正 orderId 格式后重新提交 |
| 400 | Subscription cannot be canceled, current status: X | 订单状态非 pending、active 或 past_due(如已 canceled、canceling、closed、expired) | 该订阅已不可取消 |
| 401 | Authentication failed | API Key 签名无效 | 检查认证请求头 |
| 403 | Order does not belong to user | 归属校验失败 | 验证调用方拥有该订单 |
| 404 | Order not found | 订单不存在 | 验证 order ID |
| 500 | Failed to cancel subscription | 本地写入未落地 —— 版本冲突,或换方案新订阅的 PSP 取消已受理而本地写入失败(两侧已分叉) | 重试前先重新读取订单;已分叉的换方案取消需要人工核对 |
| 500 | Internal server error | 服务端意外失败 | 指数退避重试(起步 5s,最多 3 次) |
| 502 | Failed to cancel subscription | 本地更新或 PSP 取消失败 | 指数退避重试(起步 5s,最多 3 次) |