A Chargefy permite que um unico cliente tenha multiplas assinaturas ativas ao mesmo tempo. Isso e util para cenarios como produtos complementares, add-ons, ou quando o cliente gerencia diferentes projetos com planos independentes.
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 o customerId 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: '[email protected] ' ,
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: '[email protected] ' , // 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.