> ## Documentation Index
> Fetch the complete documentation index at: https://docs.waffo.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# 创建促销码

> 为优惠券新增一个促销码

为一张 `active` 的优惠券新增一个促销码。可以传入自己的 `code`，也可以不传，由系统随机生成。

```
POST /v1/actions/promotion-code/create-promotion-code
```

**认证方式：** API Key

## 请求体

| 字段 | 类型 | 必需 | 说明 |
| - | - | - | - |
| `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" }`），小数位不能超过该币种的小数位。未列出的币种不设门槛 |

## 请求示例

```typescript SDK theme={"system"}
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"
```

```bash cURL theme={"system"}
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"
```

```python Python theme={"system"}
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)

```json theme={"system"}
{
  "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 | 由[批量生成促销码](https://docs.waffo.ai/zh/api-reference/endpoints/promotion-codes/generate-promotion-codes.md)创建时为批次 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 次），沿用同一个幂等键 |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.