> ## 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.

# 创建商品

> 创建支持多币种定价的一次性购买商品

创建支持多币种定价的一次性购买商品。

```
POST /v1/actions/onetime-product/create-product
```

**认证方式：** API Key

## 请求体

| 字段            | 类型             | 必需 | 说明                                                          |
| ------------- | -------------- | -- | ----------------------------------------------------------- |
| `storeId`     | string         | 是  | Store ID（Short ID 格式 `STO_xxx`）                             |
| `name`        | string         | 是  | 商品名称（≤ 64 字符）                                               |
| `description` | string \| null | 否  | 商品描述（支持 Markdown；传 `null` 或 `""` 清空）                        |
| `prices`      | object         | 是  | 多币种定价映射（见下方）                                                |
| `media`       | array          | 否  | 商品图片/视频（见下方）                                                |
| `successUrl`  | string \| null | 否  | 购买成功后的跳转 URL（≤ 512 字符，必须为合法 http(s) URL；传 `null` 或 `""` 清空） |
| `metadata`    | object         | 否  | 自定义键值数据（最多 50 个键）                                           |

## 价格对象格式

价格为 ISO 4217 货币代码到价格配置对象的映射。至少需要一个货币。

```json theme={"system"}
{
  "USD": {
    "amount": "29.00",
    "taxIncluded": false,
    "taxCategory": "saas"
  },
  "EUR": {
    "amount": "27.00",
    "taxIncluded": true,
    "taxCategory": "saas"
  }
}
```

| 字段            | 类型      | 说明                               |
| ------------- | ------- | -------------------------------- |
| `amount`      | string  | 显示格式的价格字符串（例如 "29.00" = \$29.00） |
| `taxIncluded` | boolean | 金额是否已包含税费                        |
| `taxCategory` | string  | 用于税率计算的税务类别                      |

**支持的 `taxCategory` 值：**

| 值                      | 说明      |
| ---------------------- | ------- |
| `digital_goods`        | 通用数字商品  |
| `saas`                 | 软件即服务   |
| `software`             | 可下载软件   |
| `ebook`                | 电子书     |
| `online_course`        | 在线课程和教育 |
| `consulting`           | 咨询服务    |
| `professional_service` | 专业服务    |

## 媒体项格式

```json theme={"system"}
{
  "type": "image",
  "url": "https://example.com/product.png",
  "alt": "Product screenshot",
  "thumbnail": "https://example.com/product-thumb.png"
}
```

| 字段          | 类型     | 说明                 |
| ----------- | ------ | ------------------ |
| `type`      | string | `image` 或 `video`  |
| `url`       | string | 媒体 URL             |
| `alt`       | string | 用于无障碍的替代文本         |
| `thumbnail` | string | 缩略图 URL（图片可选，视频推荐） |

## 请求示例

<CodeGroup>
  ```typescript SDK theme={"system"}
  import { WaffoPancake, TaxCategory } from "@waffo/pancake-ts";

  const client = new WaffoPancake({
    merchantId: process.env.WAFFO_MERCHANT_ID!,
    privateKey: process.env.WAFFO_PRIVATE_KEY!,
  });

  const { product } = await client.onetimeProducts.create({
    storeId: "STO_2aUyqjCzEIiEcYMKj7TZtw",
    name: "Premium Template Pack",
    description: "50 premium design templates for your next project.",
    prices: {
      USD: { amount: "49.00", taxIncluded: false, taxCategory: TaxCategory.DigitalGoods },
      EUR: { amount: "45.00", taxIncluded: true, taxCategory: TaxCategory.DigitalGoods },
    },
    media: [
      { type: "image", url: "https://example.com/templates-preview.png", alt: "Template preview" },
    ],
    successUrl: "https://example.com/thank-you",
    metadata: { category: "design", fileCount: "50" },
  });
  ```

  ```bash cURL theme={"system"}
  MERCHANT_ID="MER_2aUyqjCzEIiEcYMKj7TZtw"
  TIMESTAMP=$(date +%s)
  BODY='{
    "storeId": "STO_2aUyqjCzEIiEcYMKj7TZtw",
    "name": "Premium Template Pack",
    "description": "50 premium design templates for your next project.",
    "prices": {
      "USD": { "amount": "49.00", "taxIncluded": false, "taxCategory": "digital_goods" },
      "EUR": { "amount": "45.00", "taxIncluded": true, "taxCategory": "digital_goods" }
    },
    "successUrl": "https://example.com/thank-you"
  }'
  BODY_HASH=$(echo -n "$BODY" | openssl dgst -sha256 -binary | base64 -w 0)
  CANONICAL_REQUEST="POST
  /v1/actions/onetime-product/create-product
  $TIMESTAMP
  $BODY_HASH"

  SIGNATURE=$(echo -n "$CANONICAL_REQUEST" | openssl dgst -sha256 -sign private_key.pem | base64 -w 0)

  curl -X POST "https://api.waffo.ai/v1/actions/onetime-product/create-product" \
    -H "Content-Type: application/json" \
    -H "X-Merchant-Id: $MERCHANT_ID" \
    -H "X-Timestamp: $TIMESTAMP" \
    -H "X-Signature: $SIGNATURE" \
    -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 = {
      "storeId": "STO_2aUyqjCzEIiEcYMKj7TZtw",
      "name": "Premium Template Pack",
      "description": "50 premium design templates for your next project.",
      "prices": {
          "USD": {"amount": "49.00", "taxIncluded": False, "taxCategory": "digital_goods"},
          "EUR": {"amount": "45.00", "taxIncluded": True, "taxCategory": "digital_goods"},
      },
      "successUrl": "https://example.com/thank-you",
  }

  body_json = json.dumps(body, separators=(",", ":"))
  body_hash = base64.b64encode(hashlib.sha256(body_json.encode()).digest()).decode()
  path = "/v1/actions/onetime-product/create-product"
  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,
      },
      data=body_json,
  )
  print(resp.json())
  ```
</CodeGroup>

## 成功响应 (200)

```json theme={"system"}
{
  "data": {
    "product": {
      "id": "PROD_3kF9mNpQrStUvWxYz1A2bC",
      "storeId": "STO_2aUyqjCzEIiEcYMKj7TZtw",
      "name": "Premium Template Pack",
      "description": "50 premium design templates for your next project.",
      "prices": {
        "USD": { "amount": "49.00", "taxCategory": "digital_goods" },
        "EUR": { "amount": "45.00", "taxCategory": "digital_goods" }
      },
      "media": [
        { "type": "image", "url": "https://example.com/templates-preview.png", "alt": "Template preview" }
      ],
      "successUrl": "https://example.com/thank-you",
      "metadata": { "category": "design", "fileCount": "50" },
      "status": "active",
      "createdAt": "2026-01-15T10:30:00.000Z",
      "updatedAt": "2026-01-15T10:30:00.000Z"
    }
  }
}
```

## 响应字段

响应包裹在 `data.product` 中。商品对象是合并了商品和版本字段的扁平化详情视图。

| 字段            | 类型             | 说明                             |
| ------------- | -------------- | ------------------------------ |
| `id`          | string         | 商品 ID（`PROD_xxx`）              |
| `storeId`     | string         | Store ID（`STO_xxx`）            |
| `name`        | string         | 商品名称（来自当前版本）                   |
| `description` | string \| null | 商品描述（来自当前版本）                   |
| `prices`      | object         | 多币种定价映射（见下方）                   |
| `media`       | array          | 媒体项（来自当前版本）                    |
| `successUrl`  | string \| null | 成功跳转 URL（来自当前版本）               |
| `metadata`    | object         | 自定义元数据（来自当前版本）                 |
| `status`      | string         | 当前环境中的状态：`active` 或 `inactive` |
| `createdAt`   | string         | 商品创建时间戳（ISO 8601）              |
| `updatedAt`   | string         | 商品最后更新时间戳（ISO 8601）            |

**响应价格对象**（每个货币）：

| 字段            | 类型     | 说明                     |
| ------------- | ------ | ---------------------- |
| `amount`      | string | 显示格式的价格字符串（例如 "49.00"） |
| `taxCategory` | string | 用于税率计算的税务类别            |

<Note>
  `status` 字段反映您当前操作的环境。在 **test** 环境（默认）中创建时，状态为 `active`。在 **production** 环境中创建时，返回 production 状态。
</Note>

## 错误响应

> **重试策略**：4xx 一律不要重试 — 修正请求后重发。5xx 指数退避重试（起步 5s，最多 3 次）。

| 状态码 | `errors[0].message`                      | 含义                                            | 推荐处理                  |
| --- | ---------------------------------------- | --------------------------------------------- | --------------------- |
| 400 | `Missing required field: storeId`        | 未提供 `storeId`                                 | 修正请求体后重发              |
| 400 | `Invalid ID format`                      | 提供的 Short ID 无法解码                             | 修正 `storeId` 格式后重发    |
| 400 | `Missing required field: name`           | 未提供 `name`                                    | 修正请求体后重发              |
| 400 | `Prices must have at least one currency` | `prices` 缺失或为空对象                              | 至少提供一种货币后重发           |
| 400 | `Invalid currency code`                  | 货币代码必须为 3 位大写字母（ISO 4217，如 `USD`、`EUR`、`JPY`） | 使用合法 ISO 4217 代码后重发   |
| 400 | `Invalid amount`                         | 金额必须为正数字符串（如 `"9.99"`、`"100"`）                | 使用正数字符串后重发            |
| 404 | `Store not found`                        | 门店不存在或不可访问                                    | 验证 `storeId` 是否属于当前商户 |
| 500 | `Internal server error`                  | 服务端意外失败                                       | 指数退避重试（起步 5s，最多 3 次）  |
