active のクーポンにプロモーションコードを 1 つ追加します。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 のキーが 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 回)。冪等キーは同じものを使用 |