Skip to main content
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

CenarioExemplo
Produtos complementaresPlano SaaS + Plano de Suporte Premium
Add-onsPlano base + Pacote de armazenamento extra
Multiplos projetosUm plano por projeto ou workspace
Servicos independentesCRM + 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.