Recur
API 參考

Promotion Codes API

優惠碼 API — 建立、查詢、更新、停用優惠碼,驗證折扣,查詢兌換記錄

Promotion Codes API

Promotion Code(優惠碼)是面向顧客的折扣代碼,每個優惠碼連結到一個 Coupon(優惠券),由 Coupon 定義折扣規則。

除了驗證端點可使用 Publishable Key 外,其餘端點皆需 Secret Key 認證。


建立優惠碼

建立一個新的優惠碼,連結到指定的 Coupon。

端點

POST /promotion-codes

請求參數

參數類型必填說明
coupon_idstring連結的優惠券 ID
codestring優惠碼(3-20 字元,僅大寫英文與數字)
valid_fromstring生效時間(ISO 8601)
valid_untilstring到期時間(ISO 8601,需晚於 valid_from
max_redemptionsinteger全域兌換上限
max_redemptions_per_customerinteger每位顧客兌換上限(預設 1
minimum_amountinteger最低消費金額(TWD)
customer_eligibilitystringALL(預設)、NEW_CUSTOMERSEXISTING_CUSTOMERS
restricted_to_customer_idstring限定特定顧客使用
activeboolean是否啟用(預設 true
metadataobject自訂 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-codes

Query 參數

參數類型必填說明
coupon_idstring依優惠券篩選
activestringtruefalse,依啟用狀態篩選
limitinteger回傳筆數(預設 20,上限 100
offsetinteger分頁偏移量(預設 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' },
});

更新優惠碼

更新優惠碼的設定。codecoupon_id 建立後不可修改。

端點

PATCH /promotion-codes/:id

請求參數

參數類型說明
valid_fromstring生效時間(ISO 8601)
valid_untilstring到期時間(ISO 8601)
max_redemptionsinteger全域兌換上限
max_redemptions_per_customerinteger每位顧客兌換上限
minimum_amountinteger最低消費金額(TWD)
customer_eligibilitystringALLNEW_CUSTOMERSEXISTING_CUSTOMERS
activeboolean是否啟用
metadataobject自訂 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/redemptions

Query 參數

參數類型必填說明
statusstringACTIVEVOIDED
customerIdstring依顧客 ID 篩選
limitinteger回傳筆數(預設 20,上限 100
offsetinteger分頁偏移量(預設 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 KeySecret Key
客戶身份必填external_idcustomer_id選填
Rate Limit5/min per IP + 10/hr per customer60/min per API key
錯誤碼模糊化(防止碼列舉)詳細錯誤碼

使用 Publishable Key 時,必須提供 external_idcustomer_id 來識別顧客。這是為了防止匿名暴力列舉優惠碼。

請求參數

參數類型必填說明
codestring優惠碼(不區分大小寫)
external_idstring條件必填您系統中的客戶 ID
customer_idstring條件必填Recur 內部客戶 ID
product_idstring驗證是否適用於特定產品
amountnumber訂單金額(用於計算折後價)

external_idcustomer_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 回傳詳細錯誤碼,方便後端除錯。

回應欄位

欄位類型說明
validboolean優惠碼是否有效
applies_to_all_productsboolean是否適用所有產品
applicable_productsarray適用產品清單(僅限定產品時回傳)
discountobject折扣資訊
discount.typestringPERCENTAGEFIXED_AMOUNTFIRST_PERIOD_PRICE
discount.amountnumber折扣金額(需搭配 amount 參數)
discount.discounted_amountnumber折後金額
discount.labelstring人類可讀的折扣描述(如 "20% off"
discount.bonus_trial_daysnumber額外試用天數
discount.bonus_monthsnumber額外贈送月數
couponobject優惠券詳細資訊
promotion_codeobject推廣代碼詳細資訊
error_codestring錯誤碼(驗證失敗時)
error_messagestring錯誤訊息(驗證失敗時)

錯誤碼(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 實施三層防護:

  1. 客戶身份必填 — 無法匿名發送請求
  2. 雙重 Rate Limiting — IP 限制(5 次/分鐘)+ 客戶限制(10 次/小時)
  3. 錯誤碼模糊化 — 攻擊者無法區分「碼不存在」與「碼已過期」

使用 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

On this page