Visao geral do fluxo
Pre-requisitos
- Node.js 18+
- Uma conta Chargefy com Organization Access Token
- SDK instalado:
@chargefy/sdk - Ambiente de sandbox configurado para testes
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 tiporecurring 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 campoquantity. 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
| Evento | Quando ocorre |
|---|---|
subscription.updated | Quantidade de assentos alterada |
subscription.active | Assinatura 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.

