Promotion Codes API
優惠碼 API — 建立、查詢、更新、停用優惠碼,驗證折扣,查詢兌換記錄
Promotion Codes API
Promotion Code(優惠碼)是面向顧客的折扣代碼,每個優惠碼連結到一個 Coupon(優惠券),由 Coupon 定義折扣規則。
除了驗證端點可使用 Publishable Key 外,其餘端點皆需 Secret Key 認證。
建立優惠碼
建立一個新的優惠碼,連結到指定的 Coupon。
端點
POST /promotion-codes請求參數
| 參數 | 類型 | 必填 | 說明 |
|---|---|---|---|
coupon_id | string | 是 | 連結的優惠券 ID |
code | string | 是 | 優惠碼(3-20 字元,僅大寫英文與數字) |
valid_from | string | 否 | 生效時間(ISO 8601) |
valid_until | string | 否 | 到期時間(ISO 8601,需晚於 valid_from) |
max_redemptions | integer | 否 | 全域兌換上限 |
max_redemptions_per_customer | integer | 否 | 每位顧客兌換上限(預設 1) |
minimum_amount | integer | 否 | 最低消費金額(TWD) |
customer_eligibility | string | 否 | ALL(預設)、NEW_CUSTOMERS、EXISTING_CUSTOMERS |
restricted_to_customer_id | string | 否 | 限定特定顧客使用 |
active | boolean | 否 | 是否啟用(預設 true) |
metadata | object | 否 | 自訂 metadata |
請求範例
const response = await fetch('https://api.recur.tw/v1/promotion-codes', {
method: 'POST',
headers: {
'Authorization': 'Bearer sk_test_xxx',
'Content-Type': 'application/json',
},
body: JSON.stringify({
coupon_id: 'cpn_xxx',
code: 'SAVE100',
max_redemptions: 100,
valid_until: '2026-06-30T23:59:59.000Z',
customer_eligibility: 'ALL',
}),
});回應範例
{
"success": true,
"data": {
"id": "promo_xxx",
"code": "SAVE100",
"coupon_id": "cpn_xxx",
"active": true,
"valid_from": null,
"valid_until": "2026-06-30T23:59:59.000Z",
"max_redemptions": 100,
"max_redemptions_per_customer": 1,
"current_redemptions": 0,
"minimum_amount": null,
"customer_eligibility": "ALL",
"restricted_to_customer_id": null,
"metadata": null,
"created_at": "2026-03-29T10:00:00.000Z",
"updated_at": "2026-03-29T10:00:00.000Z"
}
}列出優惠碼
列出組織下的所有優惠碼。
端點
GET /promotion-codesQuery 參數
| 參數 | 類型 | 必填 | 說明 |
|---|---|---|---|
coupon_id | string | 否 | 依優惠券篩選 |
active | string | 否 | true 或 false,依啟用狀態篩選 |
limit | integer | 否 | 回傳筆數(預設 20,上限 100) |
offset | integer | 否 | 分頁偏移量(預設 0) |
請求範例
const response = await fetch(
'https://api.recur.tw/v1/promotion-codes?active=true&limit=10',
{
headers: { 'Authorization': 'Bearer sk_test_xxx' },
}
);查詢優惠碼
取得單一優惠碼的詳細資訊。
端點
GET /promotion-codes/:id請求範例
const response = await fetch('https://api.recur.tw/v1/promotion-codes/promo_xxx', {
headers: { 'Authorization': 'Bearer sk_test_xxx' },
});更新優惠碼
更新優惠碼的設定。code 和 coupon_id 建立後不可修改。
端點
PATCH /promotion-codes/:id請求參數
| 參數 | 類型 | 說明 |
|---|---|---|
valid_from | string | 生效時間(ISO 8601) |
valid_until | string | 到期時間(ISO 8601) |
max_redemptions | integer | 全域兌換上限 |
max_redemptions_per_customer | integer | 每位顧客兌換上限 |
minimum_amount | integer | 最低消費金額(TWD) |
customer_eligibility | string | ALL、NEW_CUSTOMERS、EXISTING_CUSTOMERS |
active | boolean | 是否啟用 |
metadata | object | 自訂 metadata |
所有欄位皆為選填,僅傳送需要更新的欄位。
請求範例
const response = await fetch('https://api.recur.tw/v1/promotion-codes/promo_xxx', {
method: 'PATCH',
headers: {
'Authorization': 'Bearer sk_test_xxx',
'Content-Type': 'application/json',
},
body: JSON.stringify({
max_redemptions: 200,
valid_until: '2026-12-31T23:59:59.000Z',
}),
});停用優惠碼
停用優惠碼(設為 active: false)。已兌換的記錄不受影響。
端點
DELETE /promotion-codes/:id請求範例
const response = await fetch('https://api.recur.tw/v1/promotion-codes/promo_xxx', {
method: 'DELETE',
headers: { 'Authorization': 'Bearer sk_test_xxx' },
});查詢兌換記錄
列出某個優惠碼的兌換記錄。
端點
GET /promotion-codes/:id/redemptionsQuery 參數
| 參數 | 類型 | 必填 | 說明 |
|---|---|---|---|
status | string | 否 | ACTIVE 或 VOIDED |
customerId | string | 否 | 依顧客 ID 篩選 |
limit | integer | 否 | 回傳筆數(預設 20,上限 100) |
offset | integer | 否 | 分頁偏移量(預設 0) |
請求範例
const response = await fetch(
'https://api.recur.tw/v1/promotion-codes/promo_xxx/redemptions?status=ACTIVE',
{
headers: { 'Authorization': 'Bearer sk_test_xxx' },
}
);回應範例
{
"success": true,
"data": {
"redemptions": [
{
"id": "red_xxx",
"promotion_code_id": "promo_xxx",
"customer_id": "cus_xxx",
"order_id": "ord_xxx",
"discount_amount": 100,
"status": "ACTIVE",
"redeemed_at": "2026-03-15T10:30:00.000Z"
}
],
"total": 42,
"active_count": 40,
"voided_count": 2
}
}驗證優惠碼
驗證優惠碼是否有效,並回傳折扣內容與適用產品。
端點
POST /promotion-codes/validate認證
支援 Publishable Key(前端)和 Secret Key(後端),行為有所不同:
| Publishable Key | Secret Key | |
|---|---|---|
| 客戶身份 | 必填(external_id 或 customer_id) | 選填 |
| Rate Limit | 5/min per IP + 10/hr per customer | 60/min per API key |
| 錯誤碼 | 模糊化(防止碼列舉) | 詳細錯誤碼 |
使用 Publishable Key 時,必須提供 external_id 或 customer_id 來識別顧客。這是為了防止匿名暴力列舉優惠碼。
請求參數
| 參數 | 類型 | 必填 | 說明 |
|---|---|---|---|
code | string | 是 | 優惠碼(不區分大小寫) |
external_id | string | 條件必填 | 您系統中的客戶 ID |
customer_id | string | 條件必填 | Recur 內部客戶 ID |
product_id | string | 否 | 驗證是否適用於特定產品 |
amount | number | 否 | 訂單金額(用於計算折後價) |
external_id 和 customer_id 擇一即可。使用 Publishable Key 時至少需要一個;使用 Secret Key 時兩者皆為選填。
請求範例
const response = await fetch('https://api.recur.tw/v1/promotion-codes/validate', {
method: 'POST',
headers: {
'X-Recur-Publishable-Key': 'pk_test_xxx',
'Content-Type': 'application/json',
},
body: JSON.stringify({
code: 'SAVE100',
external_id: 'user_123', // 必填:您系統的用戶 ID
product_id: 'prod_xxx', // 選填:驗證特定產品
amount: 499, // 選填:計算折後價
}),
});
const result = await response.json();const response = await fetch('https://api.recur.tw/v1/promotion-codes/validate', {
method: 'POST',
headers: {
'Authorization': 'Bearer sk_test_xxx',
'Content-Type': 'application/json',
},
body: JSON.stringify({
code: 'SAVE100',
product_id: 'prod_xxx',
amount: 499,
}),
});
const result = await response.json();回應格式
驗證成功
{
"valid": true,
"applies_to_all_products": false,
"applicable_products": [
{
"id": "prod_xxx",
"name": "超級方案",
"price": 499,
"price_after_discount": 399,
"interval": "month",
"interval_count": 1
}
],
"discount": {
"type": "FIXED_AMOUNT",
"amount": 100,
"discounted_amount": 399,
"label": "NT$100 off",
"bonus_trial_days": 7
},
"coupon": {
"name": "春季優惠",
"discount_type": "FIXED_AMOUNT",
"discount_amount": 100,
"duration": "ONCE",
"applies_to_all_products": false
},
"promotion_code": {
"code": "SAVE100",
"valid_until": "2026-06-30T23:59:59.000Z",
"max_redemptions": 100,
"current_redemptions": 42
}
}驗證失敗
{
"valid": false,
"error_code": "PROMO_CODE_INVALID",
"error_message": "此優惠碼無效"
}Publishable Key 統一回傳 PROMO_CODE_INVALID,不揭露碼是否存在、已過期或已停用,防止暴力列舉。
唯一例外:PROMO_CODE_ALREADY_USED 會保留,因為這是顧客自身的使用狀態。
{
"valid": false,
"error_code": "PROMO_CODE_EXPIRED",
"error_message": "此優惠碼已過期"
}Secret Key 回傳詳細錯誤碼,方便後端除錯。
回應欄位
| 欄位 | 類型 | 說明 |
|---|---|---|
valid | boolean | 優惠碼是否有效 |
applies_to_all_products | boolean | 是否適用所有產品 |
applicable_products | array | 適用產品清單(僅限定產品時回傳) |
discount | object | 折扣資訊 |
discount.type | string | PERCENTAGE、FIXED_AMOUNT、FIRST_PERIOD_PRICE |
discount.amount | number | 折扣金額(需搭配 amount 參數) |
discount.discounted_amount | number | 折後金額 |
discount.label | string | 人類可讀的折扣描述(如 "20% off") |
discount.bonus_trial_days | number | 額外試用天數 |
discount.bonus_months | number | 額外贈送月數 |
coupon | object | 優惠券詳細資訊 |
promotion_code | object | 推廣代碼詳細資訊 |
error_code | string | 錯誤碼(驗證失敗時) |
error_message | string | 錯誤訊息(驗證失敗時) |
錯誤碼(Secret Key)
| 錯誤碼 | 說明 |
|---|---|
PROMO_CODE_NOT_FOUND | 優惠碼不存在 |
PROMO_CODE_INACTIVE | 優惠碼或優惠券已停用 |
PROMO_CODE_EXPIRED | 優惠碼已過期 |
PROMO_CODE_NOT_STARTED | 優惠碼尚未生效 |
PROMO_CODE_EXHAUSTED | 優惠碼已達使用上限 |
PROMO_CODE_ALREADY_USED | 此顧客已使用過此優惠碼 |
PROMO_CODE_PRODUCT_MISMATCH | 優惠碼不適用於此產品 |
PROMO_CODE_MINIMUM_NOT_MET | 未達最低消費金額 |
PROMO_CODE_RESTRICTED | 優惠碼限定特定顧客使用 |
PROMO_CODE_NEW_CUSTOMERS_ONLY | 僅限新顧客使用 |
PROMO_CODE_EXISTING_CUSTOMERS_ONLY | 僅限現有顧客使用 |
安全機制
防暴力列舉
使用 Publishable Key 時,API 實施三層防護:
- 客戶身份必填 — 無法匿名發送請求
- 雙重 Rate Limiting — IP 限制(5 次/分鐘)+ 客戶限制(10 次/小時)
- 錯誤碼模糊化 — 攻擊者無法區分「碼不存在」與「碼已過期」
使用 Secret Key 時,Rate Limit 改為 60 次/分鐘 per API key,不再以 IP 為單位。這讓後端伺服器即使共用 NAT IP,也不會因為多個用戶同時驗證而被限流。
環境隔離
external_id 在 Sandbox 和 Production 環境之間完全隔離。同一個 external_id 在兩個環境中對應不同的客戶記錄,不會互相影響。
搭配 React SDK
Recur React SDK 提供 usePromoCode hook,封裝了上述 API 呼叫:
import { usePromoCode } from 'recur-tw'
function PricingPage() {
const promo = usePromoCode({ customerId: 'user_123' })
return (
<div>
<input
placeholder="輸入折扣碼"
onBlur={(e) => promo.apply(e.target.value)}
/>
{promo.isValid && <span>{promo.discount.label}</span>}
{promo.error && <span>{promo.error}</span>}
</div>
)
}usePromoCode hook 即將推出,敬請期待。
Last updated on