Recur
Recur
Recur 文件中心
開發者指南
Customer Portal後端整合前端整合
開發者指南Customer Portal

後端整合

使用 Recur Server SDK 整合 Customer Portal

後端整合

Customer Portal Session 必須在後端建立,以確保安全性。本指南說明如何使用 Recur Server SDK 整合 Portal。

安裝 SDK

npm install recur-tw
pnpm add recur-tw
yarn add recur-tw

初始化 SDK

import Recur from 'recur-tw/server';

const recur = new Recur({
  secretKey: process.env.RECUR_SECRET_KEY!,
  // 可選:自訂 API URL(開發環境)
  // baseUrl: 'http://localhost:3000/api',
});

建立 Portal Session

基本用法

const session = await recur.portal.sessions.create({
  customer: 'cus_xxxxx',
  returnUrl: 'https://your-site.com/account',
});

console.log(session.url);
// https://portal.recur.tw/s/ps_xxxxx

參數說明

參數必填說明
customer✅客戶 ID
returnUrl❌離開 Portal 後的返回 URL
configuration❌Portal 設定 ID
locale❌語言(zh-TW 或 en)

回應格式

interface PortalSession {
  id: string;              // Portal Session ID
  object: 'portal.session';
  url: string;             // Portal URL
  customer: string;        // 客戶 ID
  returnUrl: string;       // 返回 URL
  status: 'active' | 'expired';
  expiresAt: string;       // 過期時間 (ISO 8601)
  accessedAt: string | null;
  createdAt: string;
}

完整整合範例

Next.js App Router

// app/api/portal/create/route.ts
import { NextRequest, NextResponse } from 'next/server';
import Recur from 'recur-tw/server';
import { auth } from '@/lib/auth';
import { db } from '@/lib/db';

const recur = new Recur({
  secretKey: process.env.RECUR_SECRET_KEY!,
});

export async function POST(request: NextRequest) {
  try {
    // 1. 驗證用戶身份
    const session = await auth();

    if (!session?.user) {
      return NextResponse.json(
        { error: { message: 'Unauthorized' } },
        { status: 401 }
      );
    }

    // 2. 取得用戶對應的 Recur Customer ID
    const user = await db.user.findUnique({
      where: { id: session.user.id },
      select: { recurCustomerId: true },
    });

    if (!user?.recurCustomerId) {
      return NextResponse.json(
        { error: { message: 'No subscription found' } },
        { status: 404 }
      );
    }

    // 3. 建立 Portal Session
    const portalSession = await recur.portal.sessions.create({
      customer: user.recurCustomerId,
      returnUrl: `${process.env.NEXT_PUBLIC_URL}/account`,
    });

    // 4. 返回 Portal URL
    return NextResponse.json({
      url: portalSession.url,
    });
  } catch (error) {
    console.error('Portal session error:', error);

    return NextResponse.json(
      { error: { message: 'Failed to create portal session' } },
      { status: 500 }
    );
  }
}

Express.js

// routes/portal.js
const express = require('express');
const Recur = require('recur-tw/server').default;
const { authenticateUser } = require('../middleware/auth');

const router = express.Router();

const recur = new Recur({
  secretKey: process.env.RECUR_SECRET_KEY,
});

router.post('/create', authenticateUser, async (req, res) => {
  try {
    const { recurCustomerId } = req.user;

    if (!recurCustomerId) {
      return res.status(404).json({
        error: { message: 'No subscription found' }
      });
    }

    const session = await recur.portal.sessions.create({
      customer: recurCustomerId,
      returnUrl: `${process.env.APP_URL}/account`,
    });

    res.json({ url: session.url });
  } catch (error) {
    console.error('Portal session error:', error);
    res.status(500).json({
      error: { message: 'Failed to create portal session' }
    });
  }
});

module.exports = router;

Python (Flask)

# routes/portal.py
from flask import Blueprint, jsonify, request
from functools import wraps
import requests
import os

portal = Blueprint('portal', __name__)

def require_auth(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        # 實作您的認證邏輯
        user = get_current_user()
        if not user:
            return jsonify({'error': {'message': 'Unauthorized'}}), 401
        request.user = user
        return f(*args, **kwargs)
    return decorated

@portal.route('/create', methods=['POST'])
@require_auth
def create_portal_session():
    customer_id = request.user.get('recur_customer_id')

    if not customer_id:
        return jsonify({'error': {'message': 'No subscription found'}}), 404

    response = requests.post(
        'https://api.recur.tw/v1/portal/sessions',
        headers={
            'Authorization': f'Bearer {os.environ["RECUR_SECRET_KEY"]}',
            'Content-Type': 'application/json',
        },
        json={
            'customerId': customer_id,
            'returnUrl': f'{os.environ["APP_URL"]}/account',
        }
    )

    if not response.ok:
        return jsonify({'error': {'message': 'Failed to create portal session'}}), 500

    data = response.json()
    return jsonify({'url': data['url']})

錯誤處理

import { RecurAPIError } from 'recur-tw/server';

try {
  const session = await recur.portal.sessions.create({
    customer: customerId,
    returnUrl: returnUrl,
  });
} catch (error) {
  if (error instanceof RecurAPIError) {
    console.error('API Error:', error.code, error.message);

    switch (error.code) {
      case 'customer_not_found':
        // 客戶不存在
        break;
      case 'missing_return_url':
        // 未設定返回 URL
        break;
      default:
        // 其他錯誤
    }
  }
}

安全性最佳實踐

務必遵循以下安全性原則

1. 驗證用戶身份

在建立 Portal Session 前,務必驗證用戶已登入:

const session = await auth();
if (!session?.user) {
  return unauthorized();
}

2. 驗證客戶所有權

確保用戶只能存取自己的 Portal:

// 不要直接使用前端傳入的 customerId
const customerId = req.body.customerId; // ❌ 不安全

// 應該從已驗證的用戶資料取得
const customerId = user.recurCustomerId; // ✅ 安全

3. 保護 Secret Key

// ❌ 不要在前端暴露 Secret Key
const recur = new Recur({
  secretKey: 'sk_live_xxxxx', // 不要硬編碼
});

// ✅ 使用環境變數
const recur = new Recur({
  secretKey: process.env.RECUR_SECRET_KEY!,
});

下一步

  • 前端整合 - 使用 Web Component 建立 Portal 按鈕
  • Portal API 參考 - 完整 API 文件

Customer Portal

讓客戶自助管理訂閱的 Customer Portal 整合指南

前端整合

使用 Recur SDK Web Component 整合 Customer Portal 按鈕

On this page

後端整合安裝 SDK初始化 SDK建立 Portal Session基本用法參數說明回應格式完整整合範例Next.js App RouterExpress.jsPython (Flask)錯誤處理安全性最佳實踐1. 驗證用戶身份2. 驗證客戶所有權3. 保護 Secret Key下一步