active 的优惠券新增一个促销码。可以传入自己的 code,也可以不传,由系统随机生成。
POST /v1/actions/promotion-code/create-promotion-code
请求体
| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
couponId | string | 是 | 优惠券 ID(Short ID 格式 CPN_xxx)。优惠券必须为 active |
code | string | 否 | 买家输入的码:3–32 个字母、数字或 -,不区分大小写,以大写存储。不能与同一店铺、同一环境的有效码重复。不传时随机生成 10 位码 |
restrictions | object | 否 | 该码的买家限制(见下文);优惠券的 redeemBy 与 maxRedemptions 同样生效 |
限制对象
| 字段 | 类型 | 说明 |
|---|---|---|
customerEmails | string[] | 仅限这些买家(最多 100 个邮箱,不区分大小写)。为空表示不限买家 |
perCustomerLimit | integer | null | 每个买家的累计使用次数(≥ 1)。null 表示不限 |
maxRedemptions | integer | null | 本促销码的使用次数(≥ 1)。不能超过优惠券的 maxRedemptions。null 表示不额外限制 |
expiresAt | string | null | 到期时间(ISO 8601,须晚于当前时间 60 秒以上)。不能晚于优惠券的 redeemBy;不传时取优惠券的 redeemBy。优惠券已到期时返回 409 |
minimumAmount | object | null | 按币种的最低消费,按优惠前、不含税的金额判断,使用显示格式字符串(如 { "USD": "50.00" }),小数位不能超过该币种的小数位。未列出的币种不设门槛 |
请求示例
const { promotionCode } = await client.promotionCodes.create(
{
couponId: "CPN_5dXBtmF2HLlHfbPNm0Wcnz",
code: "kol-alice", // optional; a random code is generated when omitted
restrictions: {
maxRedemptions: 100,
minimumAmount: { USD: "50.00" },
},
},
{ idempotencyKey: "MER_2aUyqjCzEIiEcYMKj7TZtw-0f8e2a9c-5b1d-4e7a-9c3f-2d6b8e1a4c70" },
);
console.log(promotionCode.code); // => "KOL-ALICE"
MERCHANT_ID="MER_2aUyqjCzEIiEcYMKj7TZtw"
IDEMPOTENCY_KEY="MER_2aUyqjCzEIiEcYMKj7TZtw-0f8e2a9c-5b1d-4e7a-9c3f-2d6b8e1a4c70"
TIMESTAMP=$(date +%s)
BODY='{"couponId":"CPN_5dXBtmF2HLlHfbPNm0Wcnz","code":"kol-alice","restrictions":{"maxRedemptions":100,"minimumAmount":{"USD":"50.00"}}}'
BODY_HASH=$(echo -n "$BODY" | openssl dgst -sha256 -binary | base64 | tr -d '\n')
CANONICAL_REQUEST="POST
/v1/actions/promotion-code/create-promotion-code
$TIMESTAMP
$BODY_HASH"
SIGNATURE=$(echo -n "$CANONICAL_REQUEST" | openssl dgst -sha256 -sign private_key.pem | base64 | tr -d '\n')
curl -X POST "https://api.waffo.ai/v1/actions/promotion-code/create-promotion-code" \
-H "Content-Type: application/json" \
-H "X-Merchant-Id: $MERCHANT_ID" \
-H "X-Timestamp: $TIMESTAMP" \
-H "X-Signature: $SIGNATURE" \
-H "X-Idempotency-Key: $IDEMPOTENCY_KEY" \
-d "$BODY"
import hashlib, time, base64, json, requests
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
merchant_id = "MER_2aUyqjCzEIiEcYMKj7TZtw"
timestamp = str(int(time.time()))
body = {
"couponId": "CPN_5dXBtmF2HLlHfbPNm0Wcnz",
"code": "kol-alice",
"restrictions": {"maxRedemptions": 100, "minimumAmount": {"USD": "50.00"}},
}
body_json = json.dumps(body, separators=(",", ":"))
body_hash = base64.b64encode(hashlib.sha256(body_json.encode()).digest()).decode()
path = "/v1/actions/promotion-code/create-promotion-code"
canonical = f"POST\n{path}\n{timestamp}\n{body_hash}"
with open("private_key.pem", "rb") as f:
private_key = serialization.load_pem_private_key(f.read(), password=None)
signature = base64.b64encode(
private_key.sign(canonical.encode(), padding.PKCS1v15(), hashes.SHA256())
).decode()
resp = requests.post(
f"https://api.waffo.ai{path}",
headers={
"Content-Type": "application/json",
"X-Merchant-Id": merchant_id,
"X-Timestamp": timestamp,
"X-Signature": signature,
"X-Idempotency-Key": "MER_2aUyqjCzEIiEcYMKj7TZtw-0f8e2a9c-5b1d-4e7a-9c3f-2d6b8e1a4c70",
},
data=body_json,
)
print(resp.json())
成功响应 (200)
{
"data": {
"promotionCode": {
"id": "PMC_6eYCunG3IMmIgcQOnaXdoA",
"couponId": "CPN_5dXBtmF2HLlHfbPNm0Wcnz",
"code": "KOL-ALICE",
"status": "active",
"restrictions": {
"customerEmails": [],
"perCustomerLimit": null,
"maxRedemptions": 100,
"expiresAt": "2026-12-01T00:00:00.000Z",
"minimumAmount": { "USD": "50.00" }
},
"batchId": null,
"createdAt": "2026-11-02T08:00:00.000Z"
}
}
}
响应字段
响应包裹在data.promotionCode 中。
| 字段 | 类型 | 说明 |
|---|---|---|
id | string | 促销码 ID(PMC_xxx) |
couponId | string | 优惠券 ID(CPN_xxx) |
code | string | 码(大写) |
status | string | active 或 inactive |
restrictions | object | customerEmails、perCustomerLimit、maxRedemptions、expiresAt、minimumAmount |
batchId | string | null | 由批量生成促销码创建时为批次 ID,否则为 null |
createdAt | string | 创建时间(ISO 8601) |
错误响应
重试策略:4xx 一律不要重试 — 修正请求后重发。5xx 指数退避重试(起步 5s,最多 3 次)。重试时始终使用同一个 X-Idempotency-Key。
| 状态码 | errors[0].message | 含义 | 推荐处理 |
|---|---|---|---|
| 400 | Missing header: x-context-merchant-id | 商户上下文 header 未透传 | 修正 SDK 配置 |
| 400 | Missing or invalid header: x-context-environment | 环境 header 缺失或不是 test / prod | 把 X-Environment 设为 test 或 prod |
| 400 | Invalid JSON body | 请求体不是合法 JSON | 修正请求体后重发 |
| 400 | Missing required field: couponId | 未提供 couponId | 补齐 couponId 后重发 |
| 400 | Expected format: CPN_xxx, got "X" | couponId 不是合法 Short ID | 使用 CPN_ 前缀的 Short ID |
| 400 | code must be a string / code must be 3-32 characters of letters, digits or '-' | code 不是字符串,或含非法字符、长度不符 | 修正 code,或不传以随机生成 |
| 400 | restrictions.customerEmails[N] must be a valid email address / restrictions.customerEmails must not exceed 100 entries | customerEmails 中的第 N 个(从 0 开始)不是有效邮箱,或列表超过 100 个 | 修正列表后重发 |
| 400 | restrictions.firstTimeOnly is not supported / restrictions.dailyRedemptionLimit is not supported | restrictions 带了 firstTimeOnly 或 dailyRedemptionLimit(任意值,含 null / false),暂不支持这两个字段 | 去掉该字段 |
| 400 | restrictions.X must be an integer / restrictions.X must be at least 1 / restrictions.X must not exceed 2147483647 | perCustomerLimit 或 maxRedemptions 不是正整数或过大。X 为对应字段,如 restrictions.perCustomerLimit must be at least 1 | 改为 1 到 2147483647 之间的整数,或传 null |
| 400 | restrictions.expiresAt must be an ISO 8601 datetime | expiresAt 格式错误 | 传入带时区偏移(如 Z)的 ISO 8601 时间 |
| 400 | X: Invalid input: expected Y, received Z | 字段 X 的 JSON 类型不对,X 为完整字段路径,如 restrictions.maxRedemptions: Invalid input: expected number, received string | 按上文的类型传该字段 |
| 400 | restrictions.minimumAmount: At least one currency is required | minimumAmount 是空对象 | 至少提供一个币种,或传 null |
| 400 | restrictions.minimumAmount: Invalid currency code: "X". Must be 3 uppercase letters (e.g., "USD", "EUR", "JPY") | minimumAmount 的 key 不是合法 ISO 4217 代码 | 改为 3 位大写字母 |
| 400 | restrictions.minimumAmount: Invalid amount for X: "Y". Must be a positive number string (e.g., "5.00", "500") | minimumAmount 的值不是正数字符串 | 改为正数十进制字符串 |
| 400 | restrictions.minimumAmount: Invalid amount for X: "Y". X allows at most N decimal places / restrictions.minimumAmount: Invalid amount for X: "Y". X does not allow decimal places | minimumAmount 的金额超出该币种的小数位(如 USD "0.001"、JPY "5.5"),服务端不做四舍五入 | 按币种小数位填写(USD 最多 2 位、JPY 为整数、BHD 最多 3 位) |
| 400 | restrictions.minimumAmount: Invalid amount for X: "Y". Must be at least Z X | minimumAmount 的金额换算后不足该币种的 1 个最小单位 | 至少填写该币种的最小单位(如 USD "0.01") |
| 400 | restrictions.maxRedemptions must not exceed the coupon's maxRedemptions (X) | 上限高于优惠券的上限 | 把 maxRedemptions 调到不超过优惠券的值 |
| 400 | restrictions.expiresAt must not be later than the coupon's redeemBy (X) | 到期时间晚于优惠券的到期时间 | 使用不晚于优惠券 redeemBy 的时间 |
| 400 | restrictions.expiresAt must be more than 60 seconds in the future | 传入的 expiresAt 距当前不足 60 秒,或已经过去 | 传入晚于当前时间 60 秒以上的 expiresAt |
| 401 | Authentication required | 认证失败 | 检查 API Key、时间戳与签名 |
| 404 | Coupon not found | 优惠券不存在、属于其他商户,或在另一个环境中 | 核对 couponId 以及 API Key 所属环境 |
| 409 | Coupon is not active (status: X) / Coupon is not active | 优惠券为 pending(仍在创建中)、failed 或 inactive | 请改用有效的优惠券 |
| 409 | Coupon has expired (redeemBy: X); new promotion codes cannot be added | 优惠券已到期(距 redeemBy 不足 60 秒即视为到期),不论是否传入 expiresAt | 到期的优惠券不能再新增促销码,请新建优惠券 |
| 409 | Generated code kept colliding with existing codes; retry the request or pass a code | 未传 code 时随机生成的码多次与已有的有效码重复,本次没有创建促销码 | 用新的幂等键重试,或传入 code |
| 409 | Code "X" is already used by an active code in this store | 同一店铺、同一环境中已有有效的码(优惠券的码或促销码)使用这个码 —— 若未带幂等键重试,也包括第一次请求已创建的促销码 | 换一个码。如果是重试,请改用 GraphQL 查询该促销码 |
| 409 | Request is being processed | 使用同一 X-Idempotency-Key 的请求仍在处理中 | 等待几秒后用同一个键重试 |
| 500 | Internal server error | 服务端意外失败 | 指数退避重试(起步 5s,最多 3 次),沿用同一个幂等键 |