> ## Documentation Index
> Fetch the complete documentation index at: https://scaleup-c34c4386.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Assinaturas e Pagamentos Recorrentes

> Guia completo para implementar assinaturas e cobranças recorrentes com a Chargefy — criação de produtos, gerenciamento do ciclo de vida, webhooks e boas práticas

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

```mermaid theme={null}
sequenceDiagram
    participant Cliente
    participant SeuApp
    participant Chargefy
    participant Processador

    SeuApp->>Chargefy: Criar produto com preco recorrente
    Cliente->>SeuApp: Escolher plano
    SeuApp->>Chargefy: Criar checkout de assinatura
    Cliente->>Chargefy: Pagar (PIX ou Cartao)
    Chargefy->>Processador: Criar plano de recorrencia
    Processador-->>Chargefy: Plano ativo
    Chargefy-->>SeuApp: webhook subscription.created
    Chargefy-->>SeuApp: webhook subscription.active
    Note over Processador: A cada ciclo...
    Processador->>Chargefy: Cobranca automatica
    Chargefy-->>SeuApp: webhook subscription.active
```

## Pre-requisitos

* Node.js 18+
* Uma conta Chargefy com [Organization Access Token](https://scaleup-c34c4386.mintlify.site/integrate/oat)
* SDK instalado: `@chargefy/sdk`
* Ambiente de [sandbox](https://scaleup-c34c4386.mintlify.site/integrate/sandbox.md) configurado para testes

```bash npm theme={null}
npm install @chargefy/sdk
```

```bash pnpm theme={null}
pnpm add @chargefy/sdk
```

```typescript src/lib/chargefy.ts theme={null}
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:

| Frequencia | `recurringInterval` | Uso comum |
| - | - | - |
| Diaria | `day` | Trials, servicos por demanda |
| Semanal | `week` | Servicos semanais |
| Mensal | `month` | SaaS, streaming, planos gerais |
| Anual | `year` | Descontos para pagamento anual |

### Via SDK

```typescript theme={null}
// 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

```bash theme={null}
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"
      }
    ]
  }'
```

> [!TIP]
>
> 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

```typescript theme={null}
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

```typescript theme={null}
const checkout = await chargefy.checkouts.create({
  productPriceId: 'price_mensal_xxxx', // ID do preco recorrente
  customerEmail: 'cliente@empresa.com',
  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

```bash theme={null}
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": "cliente@empresa.com",
    "successUrl": "https://meuapp.com.br/assinatura/sucesso",
    "metadata": {
      "userId": "user_123",
      "plan": "pro"
    }
  }'
```

> [!NOTE]
>
> 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**.

> [!WARNING]
>
> **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

```typescript theme={null}
const result = await chargefy.checkouts.confirm(checkout.clientSecret, {
  customerEmail: 'cliente@empresa.com',
  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

```typescript theme={null}
const result = await chargefy.checkouts.confirm(checkout.clientSecret, {
  customerEmail: 'cliente@empresa.com',
  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)
```

> [!WARNING]
>
> Em producao, **nunca trafegue dados de cartao pelo seu servidor** sem certificacao PCI-DSS. Use o [Checkout Embed](https://scaleup-c34c4386.mintlify.site/features/checkout/embed.md) 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
```

> [!NOTE]
>
> 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

```typescript theme={null}
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}`)
}
```

```bash theme={null}
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

```typescript theme={null}
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.

```typescript theme={null}
// 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)}`)
```

```bash theme={null}
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.

```typescript theme={null}
// 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.

```typescript theme={null}
const suspended = await chargefy.subscriptions.suspend('sub_xxxx', {
  reason: 'Solicitacao do cliente — viagem',
})

console.log('Assinatura suspensa:', suspended.status) // 'suspended'
```

```bash theme={null}
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

```typescript theme={null}
const reactivated = await chargefy.subscriptions.reactivate('sub_xxxx')

console.log('Assinatura reativada:', reactivated.status) // 'active'
console.log('Proxima cobranca:', reactivated.currentPeriodEnd)
```

```bash theme={null}
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.

```typescript theme={null}
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
```

```bash theme={null}
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.

```typescript theme={null}
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:

```typescript theme={null}
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](https://scaleup-c34c4386.mintlify.site/integrate/webhooks/endpoints.md) para setup completo.

### Eventos disponíveis

| Evento | Quando ocorre |
| - | - |
| `subscription.created` | Assinatura criada apos primeiro pagamento |
| `subscription.active` | Assinatura ativada (primeiro pagamento confirmado) |
| `subscription.updated` | Alteracao de plano, preco ou metadados |
| `subscription.canceled` | Assinatura cancelada (imediato ou ao fim do periodo) |
| `subscription.revoked` | Acesso revogado (apos fim do periodo de assinatura cancelada) |

### Implementacao do handler

```typescript src/routes/webhooks.ts theme={null}
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

```typescript theme={null}
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'}`)
}
```

```bash theme={null}
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

```typescript theme={null}
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)

```typescript src/routes/subscriptions.ts theme={null}
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

```tsx src/components/SubscriptionDashboard.tsx theme={null}
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: 'user@email.com' }),
                    })
                    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.

```typescript theme={null}
// 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:

| Tentativa | Dias apos falha | Acao |
| - | - | - |
| 1a retentativa | +3 dias | Email amigavel + nova tentativa automatica |
| 2a retentativa | +7 dias | Email urgente + link para atualizar pagamento |
| 3a retentativa | +14 dias | Suspensao do acesso + email de suspensao |
| Cancelamento | +30 dias | Cancelamento definitivo se nao regularizado |

### Tratamento de pagamento falho

```typescript theme={null}
// 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`,
      })
    }
  }
}
```

> [!TIP]
>
> 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

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

### Cartoes de teste

| Cartao | Comportamento |
| - | - |
| `4111 1111 1111 1111` | Pagamento aprovado |
| `4000 0000 0000 0002` | Pagamento recusado |
| `4000 0000 0000 0069` | Pagamento expirado |
| `4000 0000 0000 0077` | Erro de processamento |

### Simular ciclo completo

```typescript theme={null}
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: 'teste@exemplo.com',
    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: 'teste@exemplo.com',
    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: 'teste@exemplo.com',
  })
  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)
```

> [!WARNING]
>
> No sandbox, as cobranças recorrentes **nao sao processadas automaticamente**. Para simular um novo ciclo, use o endpoint de teste:
>
> ```bash theme={null}
> curl -X POST https://api.chargefy.io/v1/sandbox/subscriptions/sub_xxxx/advance-cycle \
>   -H "Authorization: Bearer $CHARGEFY_ACCESS_TOKEN"
> ```

***

## Proximos passos

> [card] [Checkout Embed](https://scaleup-c34c4386.mintlify.site/features/checkout/embed.md)
>
> Integre o checkout diretamente na sua pagina.

> [card] [Webhooks](https://scaleup-c34c4386.mintlify.site/integrate/webhooks/endpoints.md)
>
> Configuracao completa de endpoints de webhook.

> [card] [Gerenciamento de clientes](https://scaleup-c34c4386.mintlify.site/features/customers)
>
> API de clientes e portal do assinante.

> [card] [Reembolsos](https://scaleup-c34c4386.mintlify.site/features/refunds.md)
>
> Como processar reembolsos de cobranças recorrentes.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.