Casos de uso
| Cenario | Exemplo |
|---|---|
| Produtos complementares | Plano SaaS + Plano de Suporte Premium |
| Add-ons | Plano base + Pacote de armazenamento extra |
| Multiplos projetos | Um plano por projeto ou workspace |
| Servicos independentes | CRM + Email Marketing como produtos separados |
Configuracao
Por padrao, a Chargefy permite multiplas assinaturas por cliente. Nao e necessaria nenhuma configuracao especial. Cada checkout cria uma nova assinatura independente, mesmo que o cliente ja tenha uma ativa.Se voce deseja restringir a uma unica assinatura por cliente, implemente essa logica no seu backend antes de criar o checkout.
Passo 1: Criar produtos independentes
Crie cada produto como uma entidade separada com seus proprios precos.Via SDK
// Produto principal
const produtoPrincipal = await chargefy.products.create({
name: 'Plataforma SaaS Pro',
description: 'Acesso completo a plataforma',
prices: [{
type: 'recurring',
amountType: 'fixed',
priceAmount: 9990, // R$ 99,90/mes
priceCurrency: 'brl',
recurringInterval: 'month',
}],
})
// Add-on: Suporte Premium
const suportePremium = await chargefy.products.create({
name: 'Suporte Premium',
description: 'Suporte prioritario com SLA de 2 horas',
prices: [{
type: 'recurring',
amountType: 'fixed',
priceAmount: 4990, // R$ 49,90/mes
priceCurrency: 'brl',
recurringInterval: 'month',
}],
})
// Add-on: Armazenamento Extra
const armazenamentoExtra = await chargefy.products.create({
name: 'Armazenamento Extra (50 GB)',
description: '50 GB adicionais de armazenamento',
prices: [{
type: 'recurring',
amountType: 'fixed',
priceAmount: 2990, // R$ 29,90/mes
priceCurrency: 'brl',
recurringInterval: 'month',
}],
})
console.log('Produto principal:', produtoPrincipal.id)
console.log('Suporte Premium:', suportePremium.id)
console.log('Armazenamento Extra:', armazenamentoExtra.id)
Passo 2: Criar checkouts para cada assinatura
Crie um checkout separado para cada produto. Use ocustomerId ou customerEmail para vincular todas as assinaturas ao mesmo cliente.
Via SDK
// Checkout do produto principal
const checkoutPrincipal = await chargefy.checkouts.create({
productPriceId: 'price_saas_pro_xxxx',
customerEmail: 'admin@empresa.com',
successUrl: 'https://meuapp.com.br/assinatura/sucesso',
})
// Checkout do add-on (mesmo cliente)
const checkoutAddon = await chargefy.checkouts.create({
productPriceId: 'price_suporte_premium_xxxx',
customerEmail: 'admin@empresa.com', // mesmo email = mesmo cliente
successUrl: 'https://meuapp.com.br/addon/sucesso',
})
Via cURL
# Checkout do add-on para cliente existente
curl -X POST https://api.chargefy.io/v1/checkouts \
-H "Authorization: Bearer $CHARGEFY_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"productPriceId": "price_suporte_premium_xxxx",
"customerId": "cus_xxxx",
"successUrl": "https://meuapp.com.br/addon/sucesso"
}'
Ao criar checkout para um cliente existente, use
customerId em vez de customerEmail. Isso garante que a assinatura sera vinculada ao cliente correto, sem risco de duplicatas.Passo 3: Listar todas as assinaturas de um cliente
Via SDK
const subscriptions = await chargefy.subscriptions.list({
customerId: 'cus_xxxx',
active: true,
limit: 20,
})
console.log(`Cliente tem ${subscriptions.items.length} assinatura(s) ativa(s):`)
let totalMensal = 0
for (const sub of subscriptions.items) {
const amount = sub.amount
totalMensal += amount
console.log(` - ${sub.product.name}: R$ ${(amount / 100).toFixed(2)}/${sub.recurringInterval}`)
}
console.log(`Total mensal: R$ ${(totalMensal / 100).toFixed(2)}`)
Via cURL
curl -X GET "https://api.chargefy.io/v1/subscriptions?customerId=cus_xxxx&active=true&limit=20" \
-H "Authorization: Bearer $CHARGEFY_ACCESS_TOKEN"
Passo 4: Gerenciar assinaturas individualmente
Cada assinatura e independente — voce pode cancelar, suspender ou alterar qualquer uma sem afetar as demais.Cancelar apenas um add-on
// Cancelar apenas o Suporte Premium, mantendo as demais
await chargefy.subscriptions.cancel('sub_suporte_xxxx', {
cancelAtPeriodEnd: true,
})
console.log('Add-on de suporte cancelado. Demais assinaturas nao foram afetadas.')
Verificar assinaturas ativas por produto
async function hasActiveSubscription(customerId: string, productId: string): Promise<boolean> {
const subs = await chargefy.subscriptions.list({
customerId,
productId,
active: true,
limit: 1,
})
return subs.items.length > 0
}
// Verificar se cliente tem suporte premium
const temSuporte = await hasActiveSubscription('cus_xxxx', 'prod_suporte_xxxx')
console.log('Tem suporte premium:', temSuporte)
Exemplo completo: Dashboard de assinaturas do cliente
Backend (Express)
src/routes/customer-subscriptions.ts
import { Router } from 'express'
import { chargefy } from '../lib/chargefy.js'
export const customerSubsRouter = Router()
// Listar todas as assinaturas do cliente
customerSubsRouter.get('/subscriptions', async (req, res) => {
try {
const customerId = req.user.chargefyCustomerId
const subscriptions = await chargefy.subscriptions.list({
customerId,
limit: 20,
})
const result = subscriptions.items.map(sub => ({
id: sub.id,
productName: sub.product.name,
productId: sub.product.id,
status: sub.status,
amount: sub.amount,
formatted: `R$ ${(sub.amount / 100).toFixed(2)}`,
interval: sub.recurringInterval,
currentPeriodEnd: sub.currentPeriodEnd,
cancelAtPeriodEnd: sub.cancelAtPeriodEnd,
isAddon: sub.product.metadata?.type === 'addon',
}))
const totalActive = result
.filter(s => s.status === 'active')
.reduce((sum, s) => sum + s.amount, 0)
res.json({
subscriptions: result,
totalMonthly: totalActive,
totalFormatted: `R$ ${(totalActive / 100).toFixed(2)}`,
})
} catch (error) {
res.status(500).json({ error: 'Falha ao listar assinaturas' })
}
})
// Listar add-ons disponiveis
customerSubsRouter.get('/available-addons', async (req, res) => {
try {
const customerId = req.user.chargefyCustomerId
// Buscar todos os add-ons
const addons = await chargefy.products.list({
metadata: { type: 'addon' },
isArchived: false,
})
// Verificar quais o cliente ja tem
const activeSubs = await chargefy.subscriptions.list({
customerId,
active: true,
})
const activeProductIds = new Set(activeSubs.items.map(s => s.product.id))
const available = addons.items.map(addon => ({
id: addon.id,
name: addon.name,
description: addon.description,
prices: addon.prices.map(p => ({
id: p.id,
amount: p.priceAmount,
formatted: `R$ ${(p.priceAmount / 100).toFixed(2)}`,
interval: p.recurringInterval,
})),
alreadySubscribed: activeProductIds.has(addon.id),
}))
res.json(available)
} catch (error) {
res.status(500).json({ error: 'Falha ao listar add-ons' })
}
})
// Assinar um add-on
customerSubsRouter.post('/subscribe-addon', async (req, res) => {
try {
const { priceId } = req.body
const customerId = req.user.chargefyCustomerId
const checkout = await chargefy.checkouts.create({
productPriceId: priceId,
customerId,
successUrl: `${process.env.FRONTEND_URL}/dashboard/addons/sucesso`,
})
res.json({ checkoutUrl: checkout.url })
} catch (error) {
res.status(400).json({ error: 'Falha ao criar checkout do add-on' })
}
})
// Cancelar uma assinatura especifica
customerSubsRouter.post('/subscriptions/:id/cancel', async (req, res) => {
try {
const { id } = req.params
const customerId = req.user.chargefyCustomerId
// Verificar se a assinatura pertence ao cliente
const sub = await chargefy.subscriptions.get(id)
if (sub.customer.id !== customerId) {
return res.status(403).json({ error: 'Assinatura nao pertence a este cliente' })
}
const canceled = await chargefy.subscriptions.cancel(id, {
cancelAtPeriodEnd: true,
})
res.json({
id: canceled.id,
status: canceled.status,
cancelAtPeriodEnd: canceled.cancelAtPeriodEnd,
accessUntil: canceled.currentPeriodEnd,
})
} catch (error) {
res.status(400).json({ error: 'Falha ao cancelar assinatura' })
}
})
Dashboard React
src/components/MultiSubscriptionDashboard.tsx
import { useState, useEffect } from 'react'
interface Subscription {
id: string
productName: string
status: string
amount: number
formatted: string
interval: string
currentPeriodEnd: string
cancelAtPeriodEnd: boolean
isAddon: boolean
}
interface Addon {
id: string
name: string
description: string
prices: { id: string; amount: number; formatted: string; interval: string }[]
alreadySubscribed: boolean
}
export function MultiSubscriptionDashboard() {
const [subscriptions, setSubscriptions] = useState<Subscription[]>([])
const [totalFormatted, setTotalFormatted] = useState('')
const [addons, setAddons] = useState<Addon[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
Promise.all([
fetch('/api/customer/subscriptions').then(r => r.json()),
fetch('/api/customer/available-addons').then(r => r.json()),
]).then(([subData, addonData]) => {
setSubscriptions(subData.subscriptions)
setTotalFormatted(subData.totalFormatted)
setAddons(addonData)
setLoading(false)
})
}, [])
async function handleCancel(subId: string) {
if (!confirm('Cancelar esta assinatura?')) return
await fetch(`/api/customer/subscriptions/${subId}/cancel`, { method: 'POST' })
window.location.reload()
}
async function handleSubscribeAddon(priceId: string) {
const res = await fetch('/api/customer/subscribe-addon', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ priceId }),
})
const { checkoutUrl } = await res.json()
window.location.href = checkoutUrl
}
if (loading) return <div>Carregando...</div>
const mainSubs = subscriptions.filter(s => !s.isAddon)
const addonSubs = subscriptions.filter(s => s.isAddon)
return (
<div style={{ maxWidth: '700px' }}>
<h2>Minhas assinaturas</h2>
<p style={{ color: '#6b7280' }}>Total mensal: <strong>{totalFormatted}</strong></p>
{/* Assinaturas principais */}
<h3>Planos</h3>
{mainSubs.map(sub => (
<div key={sub.id} style={{ border: '1px solid #e5e7eb', borderRadius: '8px', padding: '1rem', marginBottom: '0.75rem' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<strong>{sub.productName}</strong>
<p style={{ color: '#6b7280', fontSize: '0.875rem' }}>
{sub.formatted}/{sub.interval === 'month' ? 'mes' : 'ano'} — {sub.status}
</p>
</div>
{sub.status === 'active' && !sub.cancelAtPeriodEnd && (
<button onClick={() => handleCancel(sub.id)} style={{ color: '#dc2626', fontSize: '0.875rem', cursor: 'pointer', background: 'none', border: 'none' }}>
Cancelar
</button>
)}
</div>
{sub.cancelAtPeriodEnd && (
<p style={{ color: '#d97706', fontSize: '0.875rem', marginTop: '0.5rem' }}>
Cancela em {new Date(sub.currentPeriodEnd).toLocaleDateString('pt-BR')}
</p>
)}
</div>
))}
{/* Add-ons ativos */}
{addonSubs.length > 0 && (
<>
<h3 style={{ marginTop: '1.5rem' }}>Add-ons ativos</h3>
{addonSubs.map(sub => (
<div key={sub.id} style={{ border: '1px solid #e5e7eb', borderRadius: '8px', padding: '1rem', marginBottom: '0.75rem' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<strong>{sub.productName}</strong>
<span style={{ marginLeft: '0.5rem', color: '#6b7280' }}>{sub.formatted}/{sub.interval === 'month' ? 'mes' : 'ano'}</span>
</div>
<button onClick={() => handleCancel(sub.id)} style={{ color: '#dc2626', fontSize: '0.875rem', cursor: 'pointer', background: 'none', border: 'none' }}>
Remover
</button>
</div>
</div>
))}
</>
)}
{/* Add-ons disponiveis */}
{addons.filter(a => !a.alreadySubscribed).length > 0 && (
<>
<h3 style={{ marginTop: '1.5rem' }}>Add-ons disponiveis</h3>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
{addons.filter(a => !a.alreadySubscribed).map(addon => (
<div key={addon.id} style={{ border: '1px solid #e5e7eb', borderRadius: '8px', padding: '1rem' }}>
<strong>{addon.name}</strong>
<p style={{ color: '#6b7280', fontSize: '0.875rem' }}>{addon.description}</p>
{addon.prices.map(price => (
<button
key={price.id}
onClick={() => handleSubscribeAddon(price.id)}
style={{
marginTop: '0.5rem', width: '100%', padding: '0.5rem',
background: '#2563eb', color: 'white', border: 'none',
borderRadius: '4px', cursor: 'pointer',
}}
>
Adicionar — {price.formatted}/{price.interval === 'month' ? 'mes' : 'ano'}
</button>
))}
</div>
))}
</div>
</>
)}
</div>
)
}
Restringir a uma unica assinatura por produto
Se voce nao quer que o cliente assine o mesmo produto duas vezes, valide no backend:async function createCheckout(customerId: string, productPriceId: string) {
// Buscar o preco para saber qual produto
const price = await chargefy.prices.get(productPriceId)
// Verificar se ja tem assinatura ativa para este produto
const existing = await chargefy.subscriptions.list({
customerId,
productId: price.productId,
active: true,
limit: 1,
})
if (existing.items.length > 0) {
throw new Error('Cliente ja possui assinatura ativa para este produto')
}
return chargefy.checkouts.create({
productPriceId,
customerId,
successUrl: 'https://meuapp.com.br/sucesso',
})
}
Boas praticas
- Use metadata para categorizar produtos (
type: 'main',type: 'addon') e facilitar a filtragem - Valide no backend se o cliente pode assinar um add-on (ex: exigir plano principal ativo)
- Mostre o custo total mensal somando todas as assinaturas ativas
- Cancele add-ons automaticamente quando o plano principal for cancelado (se fizer sentido)
- Agrupe por tipo no dashboard para facilitar a visualizacao
Proximos passos
Variantes de Produto
Configure diferentes niveis de plano para cada produto.
Assinaturas
Guia completo de gerenciamento do ciclo de vida.

