code を持つクーポンを作成します。プロモーションコードを最大 20 個まで同時に作成できます。すべて 1 回のリクエストで作成され、クーポン、そのコード、すべてのプロモーションコードは一緒に有効になるか、どれも作成されないかのどちらかです。クーポンの使用制限は有効期限(redeemBy)と合計使用回数(maxRedemptions)のみです。購入者の制限(購入者のメールアドレス、購入者ごとの回数、最低金額)はプロモーションコードに設定します。チェックアウトセッションで couponId を指定してクーポンを適用することもできます。プロモーションコードは後からプロモーションコードの作成で追加できます。
POST /v1/actions/coupon/create-coupon
リクエストボディ
| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
storeId | string | はい | ストア ID(Short ID 形式 STO_xxx) |
name | string | はい | 内部用の名前(最大 64 文字)。購入者には表示されません |
code | string | いいえ | クーポン自身のコード。購入者がチェックアウトで入力します:英字・数字・- の 3–32 文字。大文字・小文字は区別せず、大文字で保存されます。同じストア・同じ環境の有効なコード、および promotionCodes 内のコードと重複できません。省略すると 10 文字のランダムなコードが生成されます |
discount | object | はい | { "type": "percentage", "percentOff": 20 } または { "type": "fixed_amount", "amountOff": { "USD": "5.00" } }(下記参照) |
duration | string | いいえ | サブスクリプションで割引する請求期間:once(デフォルト)、repeating、forever。単発注文では 1 回のみ割引されます |
durationInPeriods | integer | repeating の場合 | 請求期間の回数(≥ 1)。duration が repeating のときのみ指定可能 |
productIds | string[] | いいえ | 対象商品(PROD_xxx、最大 100 件)。空または省略時はストア全体が対象 |
maxRedemptions | integer | null | いいえ | クーポンのコードとすべてのプロモーションコードを合計した使用回数の上限(≥ 1)。返金しても回数は戻りません。null は無制限 |
redeemBy | string | null | いいえ | 有効期限(ISO 8601、現在から 60 秒より先の時刻)。サブスクリプションに適用済みの割引もこの時刻に終了します |
promotionCodes | object[] | いいえ | クーポンと一緒に作成するプロモーションコード、0–20 個(プロモーションコードオブジェクトを参照)。省略するとコードのないクーポンになります |
割引オブジェクト
| フィールド | 型 | 説明 |
|---|---|---|
type | string | percentage または fixed_amount |
percentOff | number | percentage の場合は必須。0 < x ≤ 100、小数第 2 位まで。100 で注文が無料になります |
amountOff | object | fixed_amount の場合は必須。ISO 4217 通貨ごとの割引額(表示形式の文字列)。小数桁数は通貨の桁数(USD 2 桁、JPY 0 桁、BHD 3 桁)以内で、超えると 400 を返し四捨五入はしない。含まれない通貨の注文には使用できません。為替換算は行いません |
{
"discount": {
"type": "fixed_amount",
"amountOff": { "USD": "5.00", "EUR": "4.50", "JPY": "800" }
}
}
プロモーションコードオブジェクト
promotionCodes の各要素は、プロモーションコードの作成と同じフィールドを受け付けます(couponId を除く):
| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
code | string | いいえ | 購入者が入力するコード:英字・数字・- の 3–32 文字。大文字・小文字は区別せず、大文字で保存されます。同じストア・同じ環境の有効なコード、およびリクエスト内の他の要素、クーポンの codeと重複できません。省略すると 10 文字のランダムなコードが生成されます |
restrictions | object | いいえ | customerEmails、perCustomerLimit、maxRedemptions(クーポンの maxRedemptions 以下)、expiresAt(現在から 60 秒より先、かつクーポンの redeemBy 以前。省略時は redeemBy)、minimumAmount — 制限オブジェクトを参照。perCustomerLimit・maxRedemptions・expiresAt・minimumAmount は null を指定でき、未設定として扱われます(expiresAt: null はクーポンの redeemBy)。customerEmails は null を受け付けません。未設定にするには省略するか、[] を指定します |
リクエスト例
import { WaffoPancake } from "@waffo/pancake-ts";
const client = new WaffoPancake({
merchantId: process.env.WAFFO_MERCHANT_ID!,
privateKey: process.env.WAFFO_PRIVATE_KEY!,
});
const { coupon } = await client.coupons.create(
{
storeId: "STO_2aUyqjCzEIiEcYMKj7TZtw",
name: "Black Friday 20% off",
discount: { type: "percentage", percentOff: 20 },
duration: "repeating",
durationInPeriods: 3,
maxRedemptions: 1000,
redeemBy: "2026-12-01T00:00:00.000Z",
code: "bf2026",
promotionCodes: [{ code: "bf-new", restrictions: { perCustomerLimit: 1 } }],
},
{ idempotencyKey: "MER_2aUyqjCzEIiEcYMKj7TZtw-0f8e2a9c-5b1d-4e7a-9c3f-2d6b8e1a4c70" },
);
console.log(coupon.status, coupon.code, coupon.promotionCodes[0].code); // => "active" "BF2026" "BF-NEW"
MERCHANT_ID="MER_2aUyqjCzEIiEcYMKj7TZtw"
IDEMPOTENCY_KEY="MER_2aUyqjCzEIiEcYMKj7TZtw-0f8e2a9c-5b1d-4e7a-9c3f-2d6b8e1a4c70"
TIMESTAMP=$(date +%s)
BODY='{"storeId":"STO_2aUyqjCzEIiEcYMKj7TZtw","name":"Black Friday 20% off","discount":{"type":"percentage","percentOff":20},"duration":"repeating","durationInPeriods":3,"maxRedemptions":1000,"redeemBy":"2026-12-01T00:00:00.000Z","code":"bf2026","promotionCodes":[{"code":"bf-new","restrictions":{"perCustomerLimit":1}}]}'
BODY_HASH=$(echo -n "$BODY" | openssl dgst -sha256 -binary | base64 | tr -d '\n')
CANONICAL_REQUEST="POST
/v1/actions/coupon/create-coupon
$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/coupon/create-coupon" \
-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 = {
"storeId": "STO_2aUyqjCzEIiEcYMKj7TZtw",
"name": "Black Friday 20% off",
"discount": {"type": "percentage", "percentOff": 20},
"duration": "repeating",
"durationInPeriods": 3,
"maxRedemptions": 1000,
"redeemBy": "2026-12-01T00:00:00.000Z",
"code": "bf2026",
"promotionCodes": [{"code": "bf-new", "restrictions": {"perCustomerLimit": 1}}],
}
body_json = json.dumps(body, separators=(",", ":"))
body_hash = base64.b64encode(hashlib.sha256(body_json.encode()).digest()).decode()
path = "/v1/actions/coupon/create-coupon"
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": {
"coupon": {
"id": "CPN_5dXBtmF2HLlHfbPNm0Wcnz",
"storeId": "STO_2aUyqjCzEIiEcYMKj7TZtw",
"name": "Black Friday 20% off",
"code": "BF2026",
"status": "active",
"discountType": "percentage",
"percentOff": 20,
"amountOff": null,
"duration": "repeating",
"durationInPeriods": 3,
"productIds": [],
"maxRedemptions": 1000,
"redeemBy": "2026-12-01T00:00:00.000Z",
"createdAt": "2026-11-01T08:00:00.000Z",
"updatedAt": "2026-11-01T08:00:00.000Z",
"promotionCodes": [
{
"id": "PMC_7fZDvoH4JNnJhdRPo2Yep1",
"couponId": "CPN_5dXBtmF2HLlHfbPNm0Wcnz",
"code": "BF-NEW",
"status": "active",
"restrictions": {
"customerEmails": [],
"perCustomerLimit": 1,
"maxRedemptions": null,
"expiresAt": "2026-12-01T00:00:00.000Z",
"minimumAmount": null
},
"batchId": null,
"createdAt": "2026-11-01T08:00:00.000Z"
}
]
}
}
}
レスポンスフィールド
レスポンスはdata.coupon にラップされます。
| フィールド | 型 | 説明 |
|---|---|---|
id | string | クーポン ID(CPN_xxx) |
storeId | string | ストア ID(STO_xxx) |
name | string | 内部用の名前 |
code | string | クーポン自身のコード(大文字)。購入者がチェックアウトで入力できます |
status | string | このレスポンスでは常に active(クーポンのステータス値を参照) |
discountType | string | percentage または fixed_amount |
percentOff | number | null | 割引率。fixed_amount の場合は null |
amountOff | object | null | 通貨ごとの割引額(表示形式の文字列)。percentage の場合、および warnings 付きレスポンスでは null |
duration | string | once、repeating、forever |
durationInPeriods | integer | null | repeating の請求期間の回数。それ以外は null |
productIds | string[] | 対象商品。空の場合はストア全体 |
maxRedemptions | integer | null | クーポンのコードとすべてのプロモーションコードを合計した使用回数の上限 |
redeemBy | string | null | 有効期限(ISO 8601) |
createdAt | string | 作成日時(ISO 8601) |
updatedAt | string | 最終更新日時(ISO 8601) |
promotionCodes | object[] | このリクエストで作成されたプロモーションコード(リクエストの順。フィールドはプロモーションコードの作成のレスポンスと同じ。warnings 付きレスポンスでは restrictions.minimumAmount が null)。指定しなかった場合は空配列 |
作成はリクエスト内で成功か失敗のどちらかに確定します。
200 レスポンスのクーポンは常に active です。決済プロバイダーが拒否した場合や作成を確認できなかった場合は 502 が返り、クーポンは failed として記録され、リクエストのコードはすべて解放されます。そのまま作成し直せます(同じコードも使用できます)。リクエストが完了前に中断された場合、クーポンが pending のままコードを占有することがあります。このようなクーポンは作成から 10 分を過ぎると、同じストア・同じ環境での次のクーポン作成、プロモーションコード作成、またはプロモーションコード一括生成のリクエスト時にその場でクリーンアップされます(failed として記録され、コードは解放されます)。warnings 付きレスポンス
クーポンがactive になった時点でそのコードは使用可能になり、リクエストはそれ以降失敗しません。その時点で Pancake が金額を表示形式に変換できなかった場合も、active のクーポンとともに 200 を返します。このとき amountOff とすべての promotionCodes[].restrictions.minimumAmount は null になり(レスポンスの形は変わりません)、レスポンスのトップレベルに金額が変換されなかったことを示す warnings 配列が付きます。その他のフィールドは通常どおり返されます。
{
"data": {
"coupon": {
"id": "CPN_5dXBtmF2HLlHfbPNm0Wcnz",
"status": "active",
"discountType": "fixed_amount",
"amountOff": null,
"...": "...",
"promotionCodes": [
{
"code": "BF-NEW",
"restrictions": { "minimumAmount": null, "...": "..." },
"...": "..."
}
]
}
},
"warnings": [
{
"message": "Coupon was created, but amounts could not be converted to display format; amountOff and restrictions.minimumAmount are returned as null",
"layer": "product",
"aiHint": "- Query: read the coupon with the GraphQL `coupon(id)` query to get the display amounts; do not retry the creation"
}
]
}
coupon(id) でクーポンを取得して確認します。
エラー
リトライポリシー: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: storeId / Missing required field: name | 必須フィールド欠落 | フィールドを追加して再送信 |
| 400 | Expected format: STO_xxx, got "X" | storeId が正しい Short ID ではない | STO_ 接頭辞付きの Short ID を使用 |
| 400 | name must not exceed 64 characters | name が長すぎる | name を短くして再送信 |
| 400 | code must be a string / promotionCodes[N].code must be a string | code、または promotionCodes の要素 N(0 始まり)の code が文字列ではない | 文字列を指定するか、省略してランダムに生成 |
| 400 | code must be 3-32 characters of letters, digits or '-' / promotionCodes[N].code must be 3-32 characters of letters, digits or '-' | code、または要素 N(0 始まり)の code に使用できない文字が含まれる、または長さが範囲外 | コードを修正して再送信 |
| 400 | promotionCodes must not exceed 20 entries | プロモーションコードが 20 個を超えている | クーポンと一緒に作成するのは 20 個まで。残りはプロモーションコードの作成で追加 |
| 400 | promotionCodes[N].code "X" duplicates another promotion code in this request | 要素 N(0 始まり)のコードがそれより前の要素と同じ(大文字・小文字は区別しない) | 重複を取り除く |
| 400 | promotionCodes[N].code "X" duplicates the coupon code | 要素 N(0 始まり)のコードが code と同じ(大文字・小文字は区別しない) | その要素に別のコードを使用 |
| 400 | promotionCodes[N].restrictions.maxRedemptions must not exceed the coupon's maxRedemptions (M) / promotionCodes[N].restrictions.expiresAt must be more than 60 seconds in the future / promotionCodes[N].restrictions.expiresAt must not be later than the coupon's redeemBy (T) | N 番目(0 始まり)の要素がクーポンの制限を超えている、またはその expiresAt が現在から 60 秒未満 | その要素の制限を修正 |
| 400 | discount.type must be percentage or fixed_amount / discount: discount.type must be percentage or fixed_amount | discount.type が許可値ではない(前者)、または discount が欠落しているかオブジェクトではない(後者) | type が percentage または fixed_amount の discount を指定 |
| 400 | discount.percentOff is required for percentage / discount.percentOff must be greater than 0 / discount.percentOff must not exceed 100 / discount.percentOff allows at most 2 decimal places | percentOff が欠落、または範囲外 | 0 < x ≤ 100、小数第 2 位までの数値を使用 |
| 400 | discount.amountOff: At least one currency is required / promotionCodes[N].restrictions.minimumAmount: At least one currency is required | amountOff、または promotionCodes の要素 N(0 始まり)の minimumAmount が空のオブジェクト | 通貨を 1 つ以上指定 |
| 400 | discount.amountOff: Invalid currency code: "X". Must be 3 uppercase letters (e.g., "USD", "EUR", "JPY") / promotionCodes[N].restrictions.minimumAmount: Invalid currency code: "X". Must be 3 uppercase letters (e.g., "USD", "EUR", "JPY") | amountOff、または要素 N の minimumAmount のキーが ISO 4217 コードに準拠していない | 3 文字の大文字を使用 |
| 400 | discount.amountOff: Invalid amount for X: "Y". Must be a positive number string (e.g., "5.00", "500") / promotionCodes[N].restrictions.minimumAmount: Invalid amount for X: "Y". Must be a positive number string (e.g., "5.00", "500") | amountOff、または要素 N の minimumAmount の値が正の数値文字列ではない | 正の十進数文字列を使用 |
| 400 | discount.amountOff: Invalid amount for X: "Y". X allows at most D decimal places / discount.amountOff: Invalid amount for X: "Y". X does not allow decimal places / promotionCodes[N].restrictions.minimumAmount: Invalid amount for X: "Y". X allows at most D decimal places / promotionCodes[N].restrictions.minimumAmount: Invalid amount for X: "Y". X does not allow decimal places | amountOff、または要素 N の minimumAmount の金額が通貨の小数桁数を超えている(例:USD "0.001"、JPY "5.5")。四捨五入は行わない | 通貨の小数桁数に合わせる(USD 2 桁、JPY 0 桁、BHD 3 桁) |
| 400 | discount.amountOff: Invalid amount for X: "Y". Must be at least Z X / promotionCodes[N].restrictions.minimumAmount: Invalid amount for X: "Y". Must be at least Z X | amountOff、または要素 N の minimumAmount の金額が通貨の最小単位 1 つ分に満たない | 少なくとも通貨の最小単位を指定(例:USD "0.01") |
| 400 | duration must be once, repeating or forever | duration が許可値ではない | once、repeating、forever のいずれかを使用 |
| 400 | durationInPeriods is required when duration is repeating, and only allowed then | repeating で durationInPeriods が欠落、または他の duration で指定されている | durationInPeriods は repeating のときのみ指定 |
| 400 | durationInPeriods must be an integer / durationInPeriods must be at least 1 / durationInPeriods must not exceed 2147483647 | durationInPeriods が正の整数ではない、または大きすぎる | 1〜2147483647 の整数を使用 |
| 400 | maxRedemptions must be an integer / maxRedemptions must be at least 1 / maxRedemptions must not exceed 2147483647 | クーポンの maxRedemptions が正の整数ではない、または大きすぎる | 1〜2147483647 の整数を使用。無制限なら null |
| 400 | promotionCodes[N].restrictions.X must be an integer / promotionCodes[N].restrictions.X must be at least 1 / promotionCodes[N].restrictions.X must not exceed 2147483647 | エントリ N(0 始まり)の X(perCustomerLimit・maxRedemptions)が正の整数ではない、または大きすぎる(例: promotionCodes[1].restrictions.perCustomerLimit must be at least 1) | 1〜2147483647 の整数を使用。無制限なら null |
| 400 | productIds must not exceed 100 entries | 商品が多すぎる | 商品 ID は最大 100 件まで |
| 400 | Expected format: PROD_xxx, got "X" | productIds に正しくない Short ID が含まれる | PROD_ 接頭辞付きの Short ID を使用 |
| 400 | redeemBy must be an ISO 8601 datetime / redeemBy must be more than 60 seconds in the future | redeemBy の形式が不正、または現在から 60 秒未満 | タイムゾーンオフセット(例:Z)付きで、現在から 60 秒より先の ISO 8601 時刻を指定 |
| 400 | restrictions is not a coupon field; set it in promotionCodes[].restrictions | 購入者の制限をクーポンに指定した。クーポンにあるのは redeemBy と maxRedemptions のみ | promotionCodes[].restrictions(または後から作成するプロモーションコード)に移す |
| 400 | dailyRedemptionLimit is not supported | クーポンに dailyRedemptionLimit を指定した(null を含む任意の値)。このフィールドはサポートされていない | フィールドを削除 |
| 400 | promotionCodes[N].restrictions.firstTimeOnly is not supported / promotionCodes[N].restrictions.dailyRedemptionLimit is not supported | 要素 N(0 始まり)の restrictions に firstTimeOnly または dailyRedemptionLimit を指定した(null / false を含む任意の値)。これらのフィールドはサポートされていない | フィールドを削除 |
| 400 | promotionCodes[N].restrictions.expiresAt must be an ISO 8601 datetime | 要素 N(0 始まり)の expiresAt の形式が正しくない | タイムゾーンオフセット(例: Z)付きの ISO 8601 時刻を指定 |
| 400 | promotionCodes[N].restrictions.customerEmails[M] must be a valid email address / promotionCodes[N].restrictions.customerEmails must not exceed 100 entries | 要素 N(0 始まり)の customerEmails の M 番目(0 始まり)が有効なメールアドレスではない、またはリストが 100 件を超える | リストを修正して再送信 |
| 400 | X: Invalid input: expected Y, received Z | フィールド X の JSON 型が正しくない。X はフィールドの完全なパス(例: promotionCodes[0].restrictions.maxRedemptions: Invalid input: expected number, received string、promotionCodes[0].restrictions.customerEmails: Invalid input: expected array, received null) | 上記の型でフィールドを指定 |
| 401 | Authentication required | 認証失敗 | API Key、タイムスタンプ、署名を確認 |
| 403 | Access denied | ストアが存在しない、または呼び出し元がそのストアの owner / admin ではない | ストアの owner または admin の API Key を使用し、storeId を確認 |
| 404 | Product not found in this store: PROD_xxx | productIds の商品がこのストアに存在しない | 商品 ID を削除または修正 |
| 409 | Code "X" is already used by an active code in this store | 同じストア・同じ環境の有効なコード(他のクーポンのコードまたはプロモーションコード)がこのコードを使用中。code とすべての promotionCodes[].code が対象で、冪等キーなしでリトライした場合は最初のリクエストで作成されたコードも含まれる。使用中のコードごとに errors に 1 件ずつ入り、何も作成されない | 別のコードを選ぶ。リトライの場合は再作成せず、GraphQL でクーポンを確認 |
| 409 | A generated code collided with an existing code; retry the request | ランダムなコード(クーポンまたはプロモーションコード)は使用中のコードを避けて生成されるが、書き込み時にそのいずれかが直前に使用された(並行リクエストなど)。Pancake は試行ごとにランダムなコードを再生成し、最大 3 回試行したがすべて衝突した。何も作成されない | リクエストをリトライ |
| 409 | Request is being processed | 同じ X-Idempotency-Key のリクエストがまだ処理中 | 数秒待ってから同じキーでリトライ |
| 502 | Payment provider rejected the coupon (code: X); its codes have been released | 決済プロバイダーがクーポンを拒否した。クーポンは failed として記録され、すべてのコードは解放済み | 割引設定を確認し、新しい冪等キーで作成し直す |
| 502 | Payment provider did not confirm the coupon creation; its codes have been released, please retry | 決済プロバイダーが作成を確認できず(タイムアウトやプロバイダーのエラー)、Pancake もプロバイダー側でクーポンを見つけられなかった。失敗として扱われ、クーポンは failed として記録され、すべてのコードは解放済み | リクエストをリトライ — 同じコードを再び使用できる |
| 500 | Internal server error | クーポンが active になる前のサーバ側の予期しない障害。クーポンがすでに記録されていた場合は failed として記録され、コードは解放される。クーポンが active になった後は 500 ではなく必ず 200 を返す(warnings 付きレスポンスを参照) | 指数バックオフでリトライ(5s 開始、最大 3 回)。冪等キーは同じものを使用 |