.agents/skills/recur-portal/SKILL.md
Implement Customer Portal for subscription self-service. Use when building account pages, letting customers manage subscriptions, update payment methods, view billing history, or when user mentions "customer portal", "帳戶管理", "訂閱管理", "更新付款方式", "self-service".
npx skillsauth add flsteven87/three_kingdoms_strategy recur-portalInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
You are helping implement Recur's Customer Portal, which allows subscribers to self-manage their subscriptions without contacting support.
This project uses FastAPI (Python) + React (TypeScript). All examples match this stack.
Customer Portal is a Recur-hosted page where customers can:
| Scenario | Solution | |----------|----------| | "Add account management page" | Create portal session and redirect | | "Let users update their card" | Portal handles payment method updates | | "Users need to cancel subscription" | Portal provides self-service cancellation | | "Show billing history" | Portal displays invoices and payments |
Portal sessions must be created server-side (requires Secret Key).
# src/api/v1/endpoints/portal.py
import httpx
import logging
from fastapi import APIRouter, Depends, HTTPException
from src.core.auth import get_current_user
from src.core.config import Settings, get_settings
from src.models.user import User
logger = logging.getLogger(__name__)
router = APIRouter()
@router.post("/portal/session")
async def create_portal_session(
current_user: User = Depends(get_current_user),
settings: Settings = Depends(get_settings),
) -> dict:
"""Create a Recur Customer Portal session for the authenticated user."""
async with httpx.AsyncClient() as client:
response = await client.post(
"https://api.recur.tw/v1/portal/sessions",
headers={
"X-Recur-Secret-Key": settings.recur_secret_key,
"Content-Type": "application/json",
},
json={
"email": current_user.email,
"returnUrl": f"{settings.frontend_url}/account",
},
)
if response.status_code == 404:
raise HTTPException(
status_code=404,
detail="找不到訂閱資料,請先訂閱方案",
)
response.raise_for_status()
data = response.json()
return {"url": data["url"]}
You can identify customers by (in priority order):
# By Recur customer ID (highest priority)
json={"customer": "cus_xxx", "returnUrl": return_url}
# By your system's user ID
json={"externalId": user.id, "returnUrl": return_url}
# By email (lowest priority)
json={"email": user.email, "returnUrl": return_url}
import { useState } from 'react'
import { Button } from '@/components/ui/button'
import { apiClient } from '@/lib/api-client'
function PortalButton() {
const [isLoading, setIsLoading] = useState(false)
const handleClick = async () => {
setIsLoading(true)
try {
const { url } = await apiClient.post<{ url: string }>('/portal/session')
window.location.href = url
} catch (error) {
console.error('Failed to open portal:', error)
// Show error toast
} finally {
setIsLoading(false)
}
}
return (
<Button onClick={handleClick} disabled={isLoading} variant="outline">
{isLoading ? '載入中...' : '管理訂閱'}
</Button>
)
}
{
"id": "portal_sess_xxx",
"object": "portal.session",
"url": "https://billing.recur.tw/portal/...", # Redirect customer here
"customer": "cus_xxx",
"returnUrl": "https://tktmanager.com/account",
"status": "active", # "active" or "expired"
"expiresAt": "2026-03-26T13:00:00Z", # Sessions last 1 hour
"accessedAt": None,
"createdAt": "2026-03-26T12:00:00Z",
}
import { useAuth } from '@/contexts/AuthContext'
import { useFeature } from '@/hooks/useFeature'
function AccountPage() {
const { user } = useAuth()
const { enabled: hasSubscription } = useFeature('pro-plan')
return (
<div className="space-y-6">
<h1 className="text-2xl font-bold">帳戶設定</h1>
<p>Email: {user?.email}</p>
<section className="space-y-2">
<h2 className="text-lg font-semibold">訂閱管理</h2>
{hasSubscription ? (
<>
<p className="text-muted-foreground">
管理您的訂閱、更新付款方式、查看帳單記錄
</p>
<PortalButton />
</>
) : (
<>
<p className="text-muted-foreground">您目前沒有訂閱</p>
<Button asChild>
<Link to="/pricing">查看方案</Link>
</Button>
</>
)}
</section>
</div>
)
}
function SubscriptionSection() {
const { enabled, entitlement } = useFeature('pro-plan')
if (!enabled) {
return (
<div>
<p>您目前沒有訂閱</p>
<Link to="/pricing">查看方案</Link>
</div>
)
}
return (
<div>
<p>
目前方案:Pro
{entitlement?.status === 'canceled' && ' (已取消,到期前仍可使用)'}
{entitlement?.status === 'past_due' && ' (付款失敗,請更新付款方式)'}
</p>
<PortalButton />
</div>
)
}
Configure portal behavior in Recur Dashboard → Settings → Customer Portal:
sk_*), never expose on frontendtry:
response = await client.post(
"https://api.recur.tw/v1/portal/sessions",
headers={"X-Recur-Secret-Key": settings.recur_secret_key, ...},
json={"email": user.email, "returnUrl": return_url},
)
response.raise_for_status()
except httpx.HTTPStatusError as e:
if e.response.status_code == 404:
# Customer doesn't exist in Recur — they haven't subscribed yet
raise HTTPException(status_code=404, detail="找不到訂閱資料") from e
raise HTTPException(status_code=502, detail="無法連接付款服務") from e
/recur-quickstart - Initial SDK setup/recur-checkout - Implement purchase flows/recur-entitlements - Check subscription access/recur-webhooks - Receive payment events (FastAPI handler)development
Set up and handle Recur webhook events for payment notifications. Use when implementing webhook handlers, verifying signatures, handling subscription events, or when user mentions "webhook", "付款通知", "訂閱事件", "payment notification".
development
Quick setup guide for Recur payment integration. Use when starting a new Recur integration, setting up API keys, installing the SDK, or when user mentions "integrate Recur", "setup Recur", "Recur 串接", "金流設定".
data-ai
List all available Recur skills and how to use them. Use when user asks "what can Recur do", "Recur skills", "Recur 有什麼功能", "help with Recur", "如何使用 Recur skills".
development
Implement access control and permission checking with Recur entitlements API. Use when building paywalls, checking subscription status, gating premium features, or when user mentions "paywall", "權限檢查", "entitlements", "access control", "premium features".