Skip to main content
A precificacao por assento permite cobrar seus clientes com base no numero de usuarios que utilizam sua plataforma. E o modelo ideal para ferramentas SaaS B2B onde o valor entregue escala com a quantidade de membros da equipe.

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 por assento

Crie um produto com preco do tipo recurring e defina o campo unitLabel para indicar que a cobranca e por unidade (assento).

Via SDK

const product = await chargefy.products.create({
  name: 'Plano Equipe',
  description: 'Colaboracao para times — cobrado por assento',
  prices: [
    {
      type: 'recurring',
      amountType: 'fixed',
      priceAmount: 4990, // R$ 49,90 por assento/mes
      priceCurrency: 'brl',
      recurringInterval: 'month',
      unitLabel: 'assento',
    },
    {
      type: 'recurring',
      amountType: 'fixed',
      priceAmount: 49900, // R$ 499,00 por assento/ano (~17% desconto)
      priceCurrency: 'brl',
      recurringInterval: 'year',
      unitLabel: 'assento',
    },
  ],
})

console.log('Produto criado:', product.id)
console.log('Preco mensal por assento:', product.prices[0].id)

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 Equipe",
    "description": "Colaboracao para times — cobrado por assento",
    "prices": [
      {
        "type": "recurring",
        "amountType": "fixed",
        "priceAmount": 4990,
        "priceCurrency": "brl",
        "recurringInterval": "month",
        "unitLabel": "assento"
      },
      {
        "type": "recurring",
        "amountType": "fixed",
        "priceAmount": 49900,
        "priceCurrency": "brl",
        "recurringInterval": "year",
        "unitLabel": "assento"
      }
    ]
  }'

Passo 2: Criar checkout com quantidade de assentos

Ao criar o checkout, informe a quantidade de assentos no campo quantity. O valor total sera calculado automaticamente.

Via SDK

const checkout = await chargefy.checkouts.create({
  productPriceId: 'price_equipe_mensal_xxxx',
  quantity: 5, // 5 assentos = 5 x R$ 49,90 = R$ 249,50/mes
  customerEmail: '[email protected]',
  successUrl: 'https://meuapp.com.br/assinatura/sucesso',
  metadata: {
    organizationId: 'org_123',
  },
})

console.log('URL do checkout:', checkout.url)
console.log('Total: R$', (4990 * 5 / 100).toFixed(2)) // R$ 249,50

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_equipe_mensal_xxxx",
    "quantity": 5,
    "customerEmail": "[email protected]",
    "successUrl": "https://meuapp.com.br/assinatura/sucesso",
    "metadata": {
      "organizationId": "org_123"
    }
  }'
Defina um numero minimo de assentos no seu backend antes de criar o checkout. Por exemplo, exija pelo menos 3 assentos para o plano Equipe.

Passo 3: Adicionar assentos (mid-cycle)

Quando um cliente precisa de mais assentos no meio do ciclo de cobranca, atualize a quantidade na assinatura. A Chargefy calcula automaticamente a prorata.

Via SDK

// Adicionar 3 assentos (de 5 para 8)
const updated = await chargefy.subscriptions.update('sub_xxxx', {
  quantity: 8, // nova quantidade total
  proration: true, // cobrar prorata dos dias restantes
})

console.log('Quantidade atualizada:', updated.quantity)
console.log('Novo valor mensal: R$', (updated.amount / 100).toFixed(2))
console.log('Prorata cobrada: R$', (updated.prorationAmount / 100).toFixed(2))

Via cURL

curl -X PATCH https://api.chargefy.io/v1/subscriptions/sub_xxxx \
  -H "Authorization: Bearer $CHARGEFY_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "quantity": 8,
    "proration": true
  }'

Calculo da prorata

A Chargefy calcula a prorata com base nos dias restantes do ciclo atual:
Exemplo: Adicionar 3 assentos no dia 15 de um ciclo mensal (30 dias)

Dias restantes: 15 de 30 = 50%
Custo dos 3 assentos novos: 3 x R$ 49,90 = R$ 149,70
Prorata cobrada: R$ 149,70 x 50% = R$ 74,85

A partir do proximo ciclo: 8 x R$ 49,90 = R$ 399,20/mes
A cobranca da prorata e processada imediatamente como uma cobranca avulsa. O valor recorrente e atualizado para o proximo ciclo.

Passo 4: Remover assentos

Para remover assentos, reduza a quantidade. Por padrao, a reducao entra em vigor no proximo ciclo de cobranca (sem reembolso do periodo atual).

Via SDK

// Remover 2 assentos (de 8 para 6)
const updated = await chargefy.subscriptions.update('sub_xxxx', {
  quantity: 6,
  proration: false, // aplica no proximo ciclo
})

console.log('Quantidade atualizada para proximo ciclo:', updated.scheduledChange?.quantity)
console.log('Quantidade atual (ate fim do ciclo):', updated.quantity)

Via cURL

curl -X PATCH https://api.chargefy.io/v1/subscriptions/sub_xxxx \
  -H "Authorization: Bearer $CHARGEFY_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "quantity": 6,
    "proration": false
  }'
Ao remover assentos, certifique-se de que o numero de usuarios ativos na organizacao nao excede a nova quantidade. Implemente essa validacao no seu backend antes de chamar a API.

Exemplo completo: Dashboard de gerenciamento de assentos

Backend (Express)

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

export const seatsRouter = Router()

// Obter informacoes de assentos da organizacao
seatsRouter.get('/info', async (req, res) => {
  try {
    const orgId = req.user.organizationId
    const subscription = await getOrgSubscription(orgId)

    if (!subscription) {
      return res.json({ hasSubscription: false })
    }

    const activeUsers = await db.users.count({ organizationId: orgId, active: true })

    res.json({
      hasSubscription: true,
      totalSeats: subscription.quantity,
      usedSeats: activeUsers,
      availableSeats: subscription.quantity - activeUsers,
      pricePerSeat: subscription.amount / subscription.quantity,
      totalAmount: subscription.amount,
      interval: subscription.recurringInterval,
    })
  } catch (error) {
    res.status(500).json({ error: 'Falha ao buscar informacoes de assentos' })
  }
})

// Adicionar assentos
seatsRouter.post('/add', async (req, res) => {
  try {
    const { seatsToAdd } = req.body
    const orgId = req.user.organizationId
    const subscription = await getOrgSubscription(orgId)

    if (!subscription) {
      return res.status(400).json({ error: 'Nenhuma assinatura ativa' })
    }

    const newQuantity = subscription.quantity + seatsToAdd

    const updated = await chargefy.subscriptions.update(subscription.id, {
      quantity: newQuantity,
      proration: true,
    })

    res.json({
      totalSeats: updated.quantity,
      prorationAmount: updated.prorationAmount,
      newMonthlyTotal: updated.amount,
    })
  } catch (error) {
    res.status(400).json({ error: 'Falha ao adicionar assentos' })
  }
})

// Remover assentos
seatsRouter.post('/remove', async (req, res) => {
  try {
    const { seatsToRemove } = req.body
    const orgId = req.user.organizationId
    const subscription = await getOrgSubscription(orgId)

    if (!subscription) {
      return res.status(400).json({ error: 'Nenhuma assinatura ativa' })
    }

    const activeUsers = await db.users.count({ organizationId: orgId, active: true })
    const newQuantity = subscription.quantity - seatsToRemove

    if (newQuantity < activeUsers) {
      return res.status(400).json({
        error: `Nao e possivel reduzir para ${newQuantity} assentos. Existem ${activeUsers} usuarios ativos.`,
      })
    }

    if (newQuantity < 1) {
      return res.status(400).json({ error: 'Minimo de 1 assento' })
    }

    const updated = await chargefy.subscriptions.update(subscription.id, {
      quantity: newQuantity,
      proration: false, // aplica no proximo ciclo
    })

    res.json({
      currentSeats: subscription.quantity,
      newSeats: newQuantity,
      effectiveDate: updated.currentPeriodEnd,
    })
  } catch (error) {
    res.status(400).json({ error: 'Falha ao remover assentos' })
  }
})

async function getOrgSubscription(orgId: string) {
  const subs = await chargefy.subscriptions.list({
    metadata: { organizationId: orgId },
    active: true,
    limit: 1,
  })
  return subs.items[0] || null
}

Dashboard React

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

interface SeatInfo {
  hasSubscription: boolean
  totalSeats: number
  usedSeats: number
  availableSeats: number
  pricePerSeat: number
  totalAmount: number
  interval: string
}

export function SeatManagement() {
  const [seatInfo, setSeatInfo] = useState<SeatInfo | null>(null)
  const [seatsToChange, setSeatsToChange] = useState(1)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    fetch('/api/seats/info')
      .then(r => r.json())
      .then(data => {
        setSeatInfo(data)
        setLoading(false)
      })
  }, [])

  async function handleAddSeats() {
    if (!seatInfo) return

    const res = await fetch('/api/seats/add', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ seatsToAdd: seatsToChange }),
    })

    if (res.ok) {
      const data = await res.json()
      alert(`${seatsToChange} assento(s) adicionado(s). Prorata: R$ ${(data.prorationAmount / 100).toFixed(2)}`)
      window.location.reload()
    }
  }

  async function handleRemoveSeats() {
    if (!seatInfo) return

    const newTotal = seatInfo.totalSeats - seatsToChange
    if (newTotal < seatInfo.usedSeats) {
      alert(`Nao e possivel. Existem ${seatInfo.usedSeats} usuarios ativos.`)
      return
    }

    const res = await fetch('/api/seats/remove', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ seatsToRemove: seatsToChange }),
    })

    if (res.ok) {
      const data = await res.json()
      alert(`Reducao agendada para ${data.effectiveDate}`)
      window.location.reload()
    }
  }

  if (loading) return <div>Carregando...</div>
  if (!seatInfo?.hasSubscription) return <div>Nenhuma assinatura ativa.</div>

  const formatCurrency = (cents: number) => `R$ ${(cents / 100).toFixed(2)}`

  return (
    <div style={{ maxWidth: '600px' }}>
      <h2>Gerenciamento de Assentos</h2>

      {/* Resumo */}
      <div style={{ border: '1px solid #e5e7eb', borderRadius: '8px', padding: '1.5rem', marginBottom: '1.5rem' }}>
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '1rem', textAlign: 'center' }}>
          <div>
            <p style={{ fontSize: '2rem', fontWeight: 'bold' }}>{seatInfo.usedSeats}</p>
            <p style={{ color: '#6b7280' }}>Em uso</p>
          </div>
          <div>
            <p style={{ fontSize: '2rem', fontWeight: 'bold' }}>{seatInfo.availableSeats}</p>
            <p style={{ color: '#6b7280' }}>Disponiveis</p>
          </div>
          <div>
            <p style={{ fontSize: '2rem', fontWeight: 'bold' }}>{seatInfo.totalSeats}</p>
            <p style={{ color: '#6b7280' }}>Total</p>
          </div>
        </div>

        <hr style={{ margin: '1rem 0' }} />

        <p>
          <strong>Valor por assento:</strong> {formatCurrency(seatInfo.pricePerSeat)}/{seatInfo.interval === 'month' ? 'mes' : 'ano'}
        </p>
        <p>
          <strong>Total mensal:</strong> {formatCurrency(seatInfo.totalAmount)}
        </p>
      </div>

      {/* Controles */}
      <div style={{ border: '1px solid #e5e7eb', borderRadius: '8px', padding: '1.5rem' }}>
        <h3>Alterar quantidade</h3>
        <div style={{ display: 'flex', alignItems: 'center', gap: '1rem', marginBottom: '1rem' }}>
          <label>Assentos:</label>
          <input
            type="number"
            min={1}
            max={100}
            value={seatsToChange}
            onChange={e => setSeatsToChange(Number(e.target.value))}
            style={{ width: '80px', padding: '0.5rem', border: '1px solid #d1d5db', borderRadius: '4px' }}
          />
        </div>

        <div style={{ display: 'flex', gap: '0.5rem' }}>
          <button
            onClick={handleAddSeats}
            style={{ padding: '0.5rem 1rem', background: '#2563eb', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}
          >
            Adicionar {seatsToChange} assento(s)
          </button>
          <button
            onClick={handleRemoveSeats}
            style={{ padding: '0.5rem 1rem', background: '#dc2626', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}
          >
            Remover {seatsToChange} assento(s)
          </button>
        </div>

        <p style={{ marginTop: '0.5rem', fontSize: '0.875rem', color: '#6b7280' }}>
          Adicionar assentos cobra prorata imediatamente. Remover aplica no proximo ciclo.
        </p>
      </div>
    </div>
  )
}

Webhooks relevantes

EventoQuando ocorre
subscription.updatedQuantidade de assentos alterada
subscription.activeAssinatura com assentos ativada

Proximos passos

Variantes de Produto

Combine assentos com diferentes niveis de plano.

Upgrade de Assinatura

Permita upgrades de plano alem de adicionar assentos.