Skip to main content
A Chargefy oferece uma solução completa para pagamentos recorrentes, gerenciando automaticamente os ciclos de cobrança. Este guia cobre desde a criação de um produto recorrente até o gerenciamento completo do ciclo de vida de assinaturas.

Visao geral do fluxo

Pre-requisitos

npm install @chargefy/sdk
src/lib/chargefy.ts
import { Chargefy } from '@chargefy/sdk'

export const chargefy = new Chargefy({
  accessToken: process.env.CHARGEFY_ACCESS_TOKEN!,
  // Para sandbox:
  // server: 'sandbox'
})

Passo 1: Criar produto com preco recorrente

O primeiro passo e criar um produto que tenha pelo menos um preco com tipo recurring. A Chargefy suporta as seguintes frequencias de cobranca:
FrequenciarecurringIntervalUso comum
DiariadayTrials, servicos por demanda
SemanalweekServicos semanais
MensalmonthSaaS, streaming, planos gerais
AnualyearDescontos para pagamento anual

Via SDK

// Criar produto com precos mensal e anual
const product = await chargefy.products.create({
  name: 'Plano Pro',
  description: 'Acesso completo a todas as funcionalidades',
  prices: [
    {
      type: 'recurring',
      amountType: 'fixed',
      priceAmount: 9990, // R$ 99,90 (em centavos)
      priceCurrency: 'brl',
      recurringInterval: 'month',
    },
    {
      type: 'recurring',
      amountType: 'fixed',
      priceAmount: 99900, // R$ 999,00 (em centavos) — economia de ~17%
      priceCurrency: 'brl',
      recurringInterval: 'year',
    },
  ],
})

console.log('Produto criado:', product.id)
console.log('Precos:', product.prices.map(p => ({
  id: p.id,
  interval: p.recurringInterval,
  amount: `R$ ${(p.priceAmount / 100).toFixed(2)}`,
})))

Via cURL

curl -X POST https://api.chargefy.io/v1/products \
  -H "Authorization: Bearer $CHARGEFY_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Plano Pro",
    "description": "Acesso completo a todas as funcionalidades",
    "prices": [
      {
        "type": "recurring",
        "amountType": "fixed",
        "priceAmount": 9990,
        "priceCurrency": "brl",
        "recurringInterval": "month"
      },
      {
        "type": "recurring",
        "amountType": "fixed",
        "priceAmount": 99900,
        "priceCurrency": "brl",
        "recurringInterval": "year"
      }
    ]
  }'
Crie multiplos precos para o mesmo produto (mensal + anual) e ofereça desconto no plano anual para incentivar compromissos mais longos.

Criar produto com periodo de trial

const productComTrial = await chargefy.products.create({
  name: 'Plano Starter',
  description: 'Comece gratuitamente por 14 dias',
  prices: [
    {
      type: 'recurring',
      amountType: 'fixed',
      priceAmount: 4990, // R$ 49,90
      priceCurrency: 'brl',
      recurringInterval: 'month',
      trialPeriodDays: 14,
    },
  ],
})

Passo 2: Criar checkout para produto de assinatura

Quando o cliente escolhe um plano, crie uma sessao de checkout usando o productPriceId do preco recorrente.

Via SDK

const checkout = await chargefy.checkouts.create({
  productPriceId: 'price_mensal_xxxx', // ID do preco recorrente
  customerEmail: '[email protected]',
  successUrl: 'https://meuapp.com.br/assinatura/sucesso',
  metadata: {
    userId: 'user_123',
    plan: 'pro',
  },
})

console.log('URL do checkout:', checkout.url)
console.log('Client secret:', checkout.clientSecret)
console.log('Expira em:', checkout.expiresAt)

Via cURL

curl -X POST https://api.chargefy.io/v1/checkouts \
  -H "Authorization: Bearer $CHARGEFY_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "productPriceId": "price_mensal_xxxx",
    "customerEmail": "[email protected]",
    "successUrl": "https://meuapp.com.br/assinatura/sucesso",
    "metadata": {
      "userId": "user_123",
      "plan": "pro"
    }
  }'
A Chargefy detecta automaticamente que o preco e recorrente e configura o checkout para criar uma assinatura apos o pagamento.

Passo 3: Processar primeiro pagamento

A primeira cobranca da assinatura e processada no checkout. O cliente pode pagar via PIX ou Cartao de Credito.
Boleto nao e recomendado para assinaturas. A compensacao lenta (1-3 dias uteis) e a baixa taxa de conversao tornam o boleto inadequado para cobranças recorrentes. Use PIX ou Cartao de Credito.

Pagamento via PIX

const result = await chargefy.checkouts.confirm(checkout.clientSecret, {
  customerEmail: '[email protected]',
  customerName: 'Maria Silva',
  paymentMethod: 'pix',
})

// Exibir QR Code para o cliente
console.log('QR Code (copia e cola):', result.paymentDetails?.pixCopyPaste)
console.log('QR Code (base64 img):', result.paymentDetails?.pixQrCodeBase64)

Pagamento via Cartao de Credito

const result = await chargefy.checkouts.confirm(checkout.clientSecret, {
  customerEmail: '[email protected]',
  customerName: 'Maria Silva',
  paymentMethod: 'credit_card',
  card: {
    cardNumber: '4111111111111111',
    holderName: 'MARIA SILVA',
    expirationMonth: '12',
    expirationYear: '2028',
    securityCode: '123',
  },
})

console.log('Status:', result.status) // 'succeeded'
console.log('ID do checkout:', result.id)
Em producao, nunca trafegue dados de cartao pelo seu servidor sem certificacao PCI-DSS. Use o Checkout Embed ou a tokenizacao client-side via Chargefy.js para enviar apenas o card_id tokenizado.

Passo 4: Plano de recorrencia (automatico)

Apos o primeiro pagamento ser confirmado, a Chargefy automaticamente:
  1. Cria um plano de recorrencia com a frequencia e valor definidos no preco
  2. Vincula o metodo de pagamento do cliente (cartao tokenizado ou conta PIX) ao plano
  3. Agenda as cobranças futuras de acordo com o intervalo configurado
  4. Emite o webhook subscription.created seguido de subscription.active
Voce nao precisa interagir diretamente com o processador de pagamentos. A Chargefy abstrai toda essa complexidade.
Primeiro pagamento confirmado
    |
    v
Chargefy cria plano de recorrencia
    |
    v
Cobranças automaticas agendadas
    |
    v
A cada ciclo: cobranca automatica → Chargefy processa → Webhook para seu app
Para assinaturas com PIX, a Chargefy gera automaticamente um novo QR Code a cada ciclo e envia por email ao cliente. Para cartao de credito, a cobranca e processada automaticamente usando o cartao tokenizado.

Gerenciamento de assinaturas

Listar assinaturas de um cliente

const subscriptions = await chargefy.subscriptions.list({
  customerId: 'cus_xxxx',
  active: true,
  page: 1,
  limit: 10,
})

for (const sub of subscriptions.items) {
  console.log(`${sub.id} | ${sub.status} | ${sub.product.name}`)
  console.log(`  Proximo ciclo: ${sub.currentPeriodEnd}`)
  console.log(`  Valor: R$ ${(sub.amount / 100).toFixed(2)}/${sub.recurringInterval}`)
}
curl -X GET "https://api.chargefy.io/v1/subscriptions?customerId=cus_xxxx&active=true" \
  -H "Authorization: Bearer $CHARGEFY_ACCESS_TOKEN"

Obter detalhes de uma assinatura

const subscription = await chargefy.subscriptions.get('sub_xxxx')

console.log({
  id: subscription.id,
  status: subscription.status,
  productName: subscription.product.name,
  priceName: subscription.price.id,
  amount: `R$ ${(subscription.amount / 100).toFixed(2)}`,
  interval: subscription.recurringInterval,
  currentPeriodStart: subscription.currentPeriodStart,
  currentPeriodEnd: subscription.currentPeriodEnd,
  cancelAtPeriodEnd: subscription.cancelAtPeriodEnd,
  customer: subscription.customer.email,
  createdAt: subscription.createdAt,
})

Upgrade de plano (com prorata)

Quando um cliente faz upgrade para um plano superior, a Chargefy calcula automaticamente o valor proporcional (prorata) restante do periodo atual e cobra a diferenca.
// Upgrade de Plano Starter (R$ 49,90/mes) para Plano Pro (R$ 99,90/mes)
const upgraded = await chargefy.subscriptions.update('sub_xxxx', {
  productPriceId: 'price_pro_mensal', // novo preco
  proration: true, // cobrar diferenca proporcional
})

console.log('Upgrade realizado:', upgraded.id)
console.log('Novo valor:', `R$ ${(upgraded.amount / 100).toFixed(2)}`)
console.log('Credito prorata:', `R$ ${(upgraded.prorationCredit / 100).toFixed(2)}`)
console.log('Cobranca adicional:', `R$ ${(upgraded.prorationAmount / 100).toFixed(2)}`)
curl -X PATCH https://api.chargefy.io/v1/subscriptions/sub_xxxx \
  -H "Authorization: Bearer $CHARGEFY_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "productPriceId": "price_pro_mensal",
    "proration": true
  }'

Downgrade de plano

No downgrade, o novo preco entra em vigor no proximo ciclo de cobranca, sem cobranca adicional no periodo atual.
// Downgrade de Plano Pro para Plano Starter no proximo ciclo
const downgraded = await chargefy.subscriptions.update('sub_xxxx', {
  productPriceId: 'price_starter_mensal',
  proration: false, // aplica no proximo ciclo
})

console.log('Downgrade agendado:', downgraded.id)
console.log('Plano atual ate:', downgraded.currentPeriodEnd)
console.log('Novo plano a partir de:', downgraded.scheduledChange?.effectiveDate)

Suspender assinatura

A suspensao interrompe temporariamente as cobranças sem cancelar a assinatura. Util para clientes com pagamento falho ou que solicitam uma pausa.
const suspended = await chargefy.subscriptions.suspend('sub_xxxx', {
  reason: 'Solicitacao do cliente — viagem',
})

console.log('Assinatura suspensa:', suspended.status) // 'suspended'
curl -X POST https://api.chargefy.io/v1/subscriptions/sub_xxxx/suspend \
  -H "Authorization: Bearer $CHARGEFY_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "Solicitacao do cliente" }'

Reativar assinatura suspensa

const reactivated = await chargefy.subscriptions.reactivate('sub_xxxx')

console.log('Assinatura reativada:', reactivated.status) // 'active'
console.log('Proxima cobranca:', reactivated.currentPeriodEnd)
curl -X POST https://api.chargefy.io/v1/subscriptions/sub_xxxx/reactivate \
  -H "Authorization: Bearer $CHARGEFY_ACCESS_TOKEN"

Cancelar assinatura

A Chargefy suporta dois modos de cancelamento:

Cancelamento ao final do periodo (recomendado)

O cliente mantem acesso ate o fim do periodo ja pago. E o padrao mais justo e reduz chargebacks.
const canceled = await chargefy.subscriptions.cancel('sub_xxxx', {
  cancelAtPeriodEnd: true, // padrao
})

console.log('Cancelamento agendado')
console.log('Acesso ate:', canceled.currentPeriodEnd)
console.log('Status:', canceled.status) // ainda 'active'
console.log('Cancel at period end:', canceled.cancelAtPeriodEnd) // true
curl -X POST https://api.chargefy.io/v1/subscriptions/sub_xxxx/cancel \
  -H "Authorization: Bearer $CHARGEFY_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "cancelAtPeriodEnd": true }'

Cancelamento imediato

O acesso e revogado imediatamente. Use apenas em casos de fraude, violacao de termos ou solicitacao explicita.
const canceled = await chargefy.subscriptions.cancel('sub_xxxx', {
  cancelAtPeriodEnd: false, // cancelamento imediato
  reason: 'Violacao dos termos de uso',
})

console.log('Cancelamento imediato')
console.log('Status:', canceled.status) // 'canceled'

Reverter cancelamento agendado

Se o cliente mudar de ideia antes do fim do periodo:
const reactivated = await chargefy.subscriptions.update('sub_xxxx', {
  cancelAtPeriodEnd: false,
})

console.log('Cancelamento revertido:', reactivated.cancelAtPeriodEnd) // false

Webhooks de assinatura

Configure um endpoint de webhooks para reagir aos eventos do ciclo de vida da assinatura. Veja o guia de webhooks para setup completo.

Eventos disponíveis

EventoQuando ocorre
subscription.createdAssinatura criada apos primeiro pagamento
subscription.activeAssinatura ativada (primeiro pagamento confirmado)
subscription.updatedAlteracao de plano, preco ou metadados
subscription.canceledAssinatura cancelada (imediato ou ao fim do periodo)
subscription.revokedAcesso revogado (apos fim do periodo de assinatura cancelada)

Implementacao do handler

src/routes/webhooks.ts
import { Router, raw } from 'express'
import crypto from 'crypto'

export const webhooksRouter = Router()
webhooksRouter.use(raw({ type: 'application/json' }))

function verifySignature(payload: Buffer, signature: string, secret: string): boolean {
  const expected = crypto.createHmac('sha256', secret).update(payload).digest('hex')
  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
}

webhooksRouter.post('/', (req, res) => {
  const signature = req.headers['webhook-signature'] as string
  const secret = process.env.CHARGEFY_WEBHOOK_SECRET!

  if (!signature || !verifySignature(req.body, signature, secret)) {
    return res.status(401).json({ error: 'Assinatura invalida' })
  }

  const event = JSON.parse(req.body.toString())

  // Responder imediatamente — processar async
  res.status(200).json({ received: true })

  // Processar evento
  handleSubscriptionEvent(event).catch(err => {
    console.error('Erro ao processar evento:', err)
  })
})

async function handleSubscriptionEvent(event: any) {
  const { type, data } = event

  switch (type) {
    case 'subscription.created':
      // Registrar assinatura no banco de dados
      await db.subscriptions.create({
        chargefy_id: data.id,
        customer_id: data.customer.id,
        product_id: data.product.id,
        status: 'pending',
        amount: data.amount,
        interval: data.recurringInterval,
        current_period_end: data.currentPeriodEnd,
      })
      console.log(`Assinatura ${data.id} criada para ${data.customer.email}`)
      break

    case 'subscription.active':
      // Liberar acesso ao servico
      await db.subscriptions.update(
        { chargefy_id: data.id },
        { status: 'active' }
      )
      await grantAccess(data.customer.id, data.product.id)
      await sendEmail(data.customer.email, 'welcome_subscription', {
        productName: data.product.name,
        amount: `R$ ${(data.amount / 100).toFixed(2)}`,
        nextBilling: data.currentPeriodEnd,
      })
      console.log(`Assinatura ${data.id} ativada`)
      break

    case 'subscription.updated':
      // Atualizar dados da assinatura (upgrade/downgrade)
      await db.subscriptions.update(
        { chargefy_id: data.id },
        {
          amount: data.amount,
          interval: data.recurringInterval,
          product_id: data.product.id,
          current_period_end: data.currentPeriodEnd,
        }
      )
      console.log(`Assinatura ${data.id} atualizada — novo valor: R$ ${(data.amount / 100).toFixed(2)}`)
      break

    case 'subscription.canceled':
      // Agendar revogacao de acesso
      await db.subscriptions.update(
        { chargefy_id: data.id },
        {
          status: 'canceled',
          cancel_at: data.cancelAtPeriodEnd ? data.currentPeriodEnd : new Date(),
        }
      )
      if (data.cancelAtPeriodEnd) {
        await sendEmail(data.customer.email, 'subscription_cancel_scheduled', {
          accessUntil: data.currentPeriodEnd,
        })
      }
      console.log(`Assinatura ${data.id} cancelada`)
      break

    case 'subscription.revoked':
      // Revogar acesso definitivamente
      await db.subscriptions.update(
        { chargefy_id: data.id },
        { status: 'revoked' }
      )
      await revokeAccess(data.customer.id, data.product.id)
      await sendEmail(data.customer.email, 'subscription_revoked', {
        productName: data.product.name,
      })
      console.log(`Assinatura ${data.id} revogada — acesso removido`)
      break
  }
}

Gerenciamento de faturas

Cada cobranca recorrente gera uma fatura (invoice). Voce pode consultar o historico de faturas do cliente.

Listar faturas de uma assinatura

const invoices = await chargefy.subscriptions.listInvoices('sub_xxxx', {
  page: 1,
  limit: 12,
})

for (const invoice of invoices.items) {
  console.log(`${invoice.id} | ${invoice.status} | R$ ${(invoice.amount / 100).toFixed(2)}`)
  console.log(`  Periodo: ${invoice.periodStart}${invoice.periodEnd}`)
  console.log(`  Pago em: ${invoice.paidAt || 'pendente'}`)
}
curl -X GET "https://api.chargefy.io/v1/subscriptions/sub_xxxx/invoices?page=1&limit=12" \
  -H "Authorization: Bearer $CHARGEFY_ACCESS_TOKEN"

Consultar fatura especifica

const invoice = await chargefy.invoices.get('inv_xxxx')

console.log({
  id: invoice.id,
  status: invoice.status, // 'paid', 'pending', 'overdue', 'void'
  amount: `R$ ${(invoice.amount / 100).toFixed(2)}`,
  paymentMethod: invoice.paymentMethod,
  periodStart: invoice.periodStart,
  periodEnd: invoice.periodEnd,
  paidAt: invoice.paidAt,
  invoiceUrl: invoice.url, // URL para visualizar a fatura
})

Exemplo completo: Sistema SaaS de assinaturas

Um exemplo funcional com backend Express e dashboard React.

Backend (Express)

src/routes/subscriptions.ts
import { Router } from 'express'
import { chargefy } from '../lib/chargefy.js'

export const subscriptionsRouter = Router()

// Listar planos disponiveis
subscriptionsRouter.get('/plans', async (_req, res) => {
  try {
    const products = await chargefy.products.list({
      isArchived: false,
      isRecurring: true,
    })

    const plans = products.items.map(product => ({
      id: product.id,
      name: product.name,
      description: product.description,
      prices: product.prices
        .filter(p => p.type === 'recurring')
        .map(p => ({
          id: p.id,
          amount: p.priceAmount,
          formatted: `R$ ${(p.priceAmount / 100).toFixed(2)}`,
          interval: p.recurringInterval,
          trialDays: p.trialPeriodDays || 0,
        })),
    }))

    res.json(plans)
  } catch (error) {
    res.status(500).json({ error: 'Falha ao listar planos' })
  }
})

// Criar checkout de assinatura
subscriptionsRouter.post('/subscribe', async (req, res) => {
  try {
    const { priceId, customerEmail } = req.body

    const checkout = await chargefy.checkouts.create({
      productPriceId: priceId,
      customerEmail,
      successUrl: `${process.env.FRONTEND_URL}/dashboard/assinatura/sucesso`,
      metadata: { source: 'pricing_page' },
    })

    res.json({ checkoutUrl: checkout.url, clientSecret: checkout.clientSecret })
  } catch (error) {
    res.status(400).json({ error: 'Falha ao criar checkout' })
  }
})

// Obter assinatura ativa do usuario
subscriptionsRouter.get('/me', async (req, res) => {
  try {
    const customerId = req.user.chargefyCustomerId // do seu middleware de auth

    const subscriptions = await chargefy.subscriptions.list({
      customerId,
      active: true,
      limit: 1,
    })

    if (subscriptions.items.length === 0) {
      return res.json({ subscription: null })
    }

    const sub = subscriptions.items[0]
    res.json({
      subscription: {
        id: sub.id,
        status: sub.status,
        plan: sub.product.name,
        amount: `R$ ${(sub.amount / 100).toFixed(2)}`,
        interval: sub.recurringInterval,
        currentPeriodEnd: sub.currentPeriodEnd,
        cancelAtPeriodEnd: sub.cancelAtPeriodEnd,
      },
    })
  } catch (error) {
    res.status(500).json({ error: 'Falha ao buscar assinatura' })
  }
})

// Trocar plano (upgrade/downgrade)
subscriptionsRouter.post('/:id/change-plan', async (req, res) => {
  try {
    const { id } = req.params
    const { newPriceId } = req.body

    // Buscar assinatura atual para determinar se e upgrade ou downgrade
    const current = await chargefy.subscriptions.get(id)
    const newPrice = await chargefy.prices.get(newPriceId)

    const isUpgrade = newPrice.priceAmount > current.amount
    const updated = await chargefy.subscriptions.update(id, {
      productPriceId: newPriceId,
      proration: isUpgrade, // prorata apenas no upgrade
    })

    res.json({
      subscription: updated,
      changeType: isUpgrade ? 'upgrade' : 'downgrade',
      effectiveDate: isUpgrade ? 'imediato' : updated.currentPeriodEnd,
    })
  } catch (error) {
    res.status(400).json({ error: 'Falha ao trocar plano' })
  }
})

// Cancelar assinatura
subscriptionsRouter.post('/:id/cancel', async (req, res) => {
  try {
    const { id } = req.params
    const { immediate } = req.body

    const canceled = await chargefy.subscriptions.cancel(id, {
      cancelAtPeriodEnd: !immediate,
    })

    res.json({
      status: canceled.status,
      cancelAtPeriodEnd: canceled.cancelAtPeriodEnd,
      accessUntil: canceled.cancelAtPeriodEnd ? canceled.currentPeriodEnd : null,
    })
  } catch (error) {
    res.status(400).json({ error: 'Falha ao cancelar assinatura' })
  }
})

// Reverter cancelamento
subscriptionsRouter.post('/:id/reactivate', async (req, res) => {
  try {
    const { id } = req.params

    const reactivated = await chargefy.subscriptions.update(id, {
      cancelAtPeriodEnd: false,
    })

    res.json({
      status: reactivated.status,
      cancelAtPeriodEnd: reactivated.cancelAtPeriodEnd,
    })
  } catch (error) {
    res.status(400).json({ error: 'Falha ao reativar assinatura' })
  }
})

// Historico de faturas
subscriptionsRouter.get('/:id/invoices', async (req, res) => {
  try {
    const { id } = req.params
    const invoices = await chargefy.subscriptions.listInvoices(id, {
      page: 1,
      limit: 12,
    })

    res.json(invoices)
  } catch (error) {
    res.status(500).json({ error: 'Falha ao listar faturas' })
  }
})

Dashboard React

src/components/SubscriptionDashboard.tsx
import { useState, useEffect } from 'react'

interface Subscription {
  id: string
  status: string
  plan: string
  amount: string
  interval: string
  currentPeriodEnd: string
  cancelAtPeriodEnd: boolean
}

interface Plan {
  id: string
  name: string
  prices: { id: string; amount: number; formatted: string; interval: string }[]
}

export function SubscriptionDashboard() {
  const [subscription, setSubscription] = useState<Subscription | null>(null)
  const [plans, setPlans] = useState<Plan[]>([])
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    Promise.all([
      fetch('/api/subscriptions/me').then(r => r.json()),
      fetch('/api/subscriptions/plans').then(r => r.json()),
    ]).then(([subData, plansData]) => {
      setSubscription(subData.subscription)
      setPlans(plansData)
      setLoading(false)
    })
  }, [])

  async function handleCancel() {
    if (!subscription) return
    if (!confirm('Tem certeza que deseja cancelar sua assinatura?')) return

    const res = await fetch(`/api/subscriptions/${subscription.id}/cancel`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ immediate: false }),
    })

    const data = await res.json()
    setSubscription(prev => prev ? {
      ...prev,
      cancelAtPeriodEnd: data.cancelAtPeriodEnd,
    } : null)
  }

  async function handleReactivate() {
    if (!subscription) return

    const res = await fetch(`/api/subscriptions/${subscription.id}/reactivate`, {
      method: 'POST',
    })

    const data = await res.json()
    setSubscription(prev => prev ? {
      ...prev,
      cancelAtPeriodEnd: data.cancelAtPeriodEnd,
      status: data.status,
    } : null)
  }

  async function handleChangePlan(priceId: string) {
    if (!subscription) return

    const res = await fetch(`/api/subscriptions/${subscription.id}/change-plan`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ newPriceId: priceId }),
    })

    const data = await res.json()
    alert(`${data.changeType === 'upgrade' ? 'Upgrade' : 'Downgrade'} realizado!`)
    window.location.reload()
  }

  if (loading) return <div>Carregando...</div>

  if (!subscription) {
    return (
      <div>
        <h2>Escolha um plano</h2>
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))', gap: '1rem' }}>
          {plans.map(plan => (
            <div key={plan.id} style={{ border: '1px solid #e5e7eb', borderRadius: '8px', padding: '1.5rem' }}>
              <h3>{plan.name}</h3>
              {plan.prices.map(price => (
                <div key={price.id} style={{ marginTop: '0.5rem' }}>
                  <p>{price.formatted}/{price.interval === 'month' ? 'mes' : 'ano'}</p>
                  <button onClick={async () => {
                    const res = await fetch('/api/subscriptions/subscribe', {
                      method: 'POST',
                      headers: { 'Content-Type': 'application/json' },
                      body: JSON.stringify({ priceId: price.id, customerEmail: '[email protected]' }),
                    })
                    const { checkoutUrl } = await res.json()
                    window.location.href = checkoutUrl
                  }}>
                    Assinar
                  </button>
                </div>
              ))}
            </div>
          ))}
        </div>
      </div>
    )
  }

  return (
    <div>
      <h2>Minha assinatura</h2>

      <div style={{ border: '1px solid #e5e7eb', borderRadius: '8px', padding: '1.5rem' }}>
        <p><strong>Plano:</strong> {subscription.plan}</p>
        <p><strong>Valor:</strong> {subscription.amount}/{subscription.interval === 'month' ? 'mes' : 'ano'}</p>
        <p><strong>Status:</strong> {subscription.status}</p>
        <p><strong>Proximo ciclo:</strong> {new Date(subscription.currentPeriodEnd).toLocaleDateString('pt-BR')}</p>

        {subscription.cancelAtPeriodEnd && (
          <div style={{ background: '#fef3c7', padding: '1rem', borderRadius: '4px', marginTop: '1rem' }}>
            <p>Sua assinatura sera cancelada em {new Date(subscription.currentPeriodEnd).toLocaleDateString('pt-BR')}.</p>
            <button onClick={handleReactivate}>Manter assinatura</button>
          </div>
        )}

        <div style={{ marginTop: '1rem', display: 'flex', gap: '0.5rem' }}>
          {!subscription.cancelAtPeriodEnd && (
            <button onClick={handleCancel} style={{ color: 'red' }}>
              Cancelar assinatura
            </button>
          )}
        </div>
      </div>

      <h3 style={{ marginTop: '2rem' }}>Trocar plano</h3>
      <div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap' }}>
        {plans.flatMap(plan =>
          plan.prices.map(price => (
            <button
              key={price.id}
              onClick={() => handleChangePlan(price.id)}
              style={{ padding: '0.5rem 1rem', border: '1px solid #e5e7eb', borderRadius: '4px' }}
            >
              {plan.name}{price.formatted}/{price.interval === 'month' ? 'mes' : 'ano'}
            </button>
          ))
        )}
      </div>
    </div>
  )
}

Boas praticas

Periodo de graca (grace period)

Configure um periodo de graca para lidar com falhas temporarias de pagamento antes de suspender o acesso.
// No handler de webhook — subscription.payment_failed
async function handlePaymentFailed(data: any) {
  const subscription = await db.subscriptions.findOne({
    chargefy_id: data.subscriptionId,
  })

  const failCount = (subscription.consecutive_failures || 0) + 1

  await db.subscriptions.update(
    { chargefy_id: data.subscriptionId },
    { consecutive_failures: failCount }
  )

  if (failCount === 1) {
    // Primeira falha — enviar notificacao amigavel
    await sendEmail(subscription.customer_email, 'payment_failed_first', {
      retryDate: data.nextRetryAt,
    })
  } else if (failCount === 2) {
    // Segunda falha — aviso mais urgente
    await sendEmail(subscription.customer_email, 'payment_failed_second', {
      updatePaymentUrl: `${process.env.FRONTEND_URL}/atualizar-pagamento`,
    })
  } else if (failCount >= 3) {
    // Terceira falha — suspender acesso
    await chargefy.subscriptions.suspend(data.subscriptionId, {
      reason: 'Pagamento falho apos 3 tentativas',
    })
    await sendEmail(subscription.customer_email, 'subscription_suspended')
  }
}

Dunning (recuperacao de receita)

Estrategia recomendada para tentativas de cobranca:
TentativaDias apos falhaAcao
1a retentativa+3 diasEmail amigavel + nova tentativa automatica
2a retentativa+7 diasEmail urgente + link para atualizar pagamento
3a retentativa+14 diasSuspensao do acesso + email de suspensao
Cancelamento+30 diasCancelamento definitivo se nao regularizado

Tratamento de pagamento falho

// Verificar pagamentos pendentes e notificar
async function checkOverdueSubscriptions() {
  const overdue = await chargefy.subscriptions.list({
    status: 'past_due',
    limit: 100,
  })

  for (const sub of overdue.items) {
    const daysPastDue = Math.floor(
      (Date.now() - new Date(sub.currentPeriodEnd).getTime()) / (1000 * 60 * 60 * 24)
    )

    if (daysPastDue > 30) {
      await chargefy.subscriptions.cancel(sub.id, {
        cancelAtPeriodEnd: false,
        reason: `Pagamento em atraso por ${daysPastDue} dias`,
      })
    }
  }
}
Ofereça um link direto para o cliente atualizar o metodo de pagamento. Isso aumenta significativamente a taxa de recuperacao de cobranças falhas.

Testando assinaturas no sandbox

O ambiente de sandbox permite testar todo o fluxo de assinatura sem cobranças reais.

Configurar cliente para sandbox

const chargefy = new Chargefy({
  accessToken: process.env.CHARGEFY_ACCESS_TOKEN!,
  server: 'sandbox',
})

Cartoes de teste

CartaoComportamento
4111 1111 1111 1111Pagamento aprovado
4000 0000 0000 0002Pagamento recusado
4000 0000 0000 0069Pagamento expirado
4000 0000 0000 0077Erro de processamento

Simular ciclo completo

import { Chargefy } from '@chargefy/sdk'

const chargefy = new Chargefy({
  accessToken: process.env.CHARGEFY_ACCESS_TOKEN!,
  server: 'sandbox',
})

async function testSubscriptionFlow() {
  // 1. Criar produto recorrente
  const product = await chargefy.products.create({
    name: '[TESTE] Plano Premium',
    prices: [{
      type: 'recurring',
      amountType: 'fixed',
      priceAmount: 1990, // R$ 19,90
      priceCurrency: 'brl',
      recurringInterval: 'month',
    }],
  })
  console.log('Produto criado:', product.id)

  // 2. Criar checkout
  const checkout = await chargefy.checkouts.create({
    productPriceId: product.prices[0].id,
    customerEmail: '[email protected]',
    successUrl: 'http://localhost:3000/sucesso',
  })
  console.log('Checkout criado:', checkout.clientSecret)

  // 3. Confirmar pagamento (sandbox — cartao de teste)
  const result = await chargefy.checkouts.confirm(checkout.clientSecret, {
    customerEmail: '[email protected]',
    customerName: 'Usuario Teste',
    paymentMethod: 'credit_card',
    card: {
      cardNumber: '4111111111111111',
      holderName: 'USUARIO TESTE',
      expirationMonth: '12',
      expirationYear: '2028',
      securityCode: '123',
    },
  })
  console.log('Pagamento:', result.status)

  // 4. Aguardar webhook subscription.active (em ambiente real)
  // No sandbox, podemos consultar diretamente
  const subscriptions = await chargefy.subscriptions.list({
    customerEmail: '[email protected]',
  })
  const sub = subscriptions.items[0]
  console.log('Assinatura:', sub.id, sub.status)

  // 5. Testar cancelamento
  const canceled = await chargefy.subscriptions.cancel(sub.id, {
    cancelAtPeriodEnd: true,
  })
  console.log('Cancelamento agendado para:', canceled.currentPeriodEnd)

  // 6. Reverter cancelamento
  const reactivated = await chargefy.subscriptions.update(sub.id, {
    cancelAtPeriodEnd: false,
  })
  console.log('Reativado:', reactivated.cancelAtPeriodEnd) // false

  // 7. Limpar (opcional)
  await chargefy.subscriptions.cancel(sub.id, { cancelAtPeriodEnd: false })
  console.log('Teste concluido com sucesso!')
}

testSubscriptionFlow().catch(console.error)
No sandbox, as cobranças recorrentes nao sao processadas automaticamente. Para simular um novo ciclo, use o endpoint de teste:
curl -X POST https://api.chargefy.io/v1/sandbox/subscriptions/sub_xxxx/advance-cycle \
  -H "Authorization: Bearer $CHARGEFY_ACCESS_TOKEN"

Proximos passos

Checkout Embed

Integre o checkout diretamente na sua pagina.

Webhooks

Configuracao completa de endpoints de webhook.

Gerenciamento de clientes

API de clientes e portal do assinante.

Reembolsos

Como processar reembolsos de cobranças recorrentes.