> ## 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` のクーポンにプロモーションコードを 1 つ追加します。`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/ja/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` のキーが 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 0 桁、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.