Documentation Index Fetch the complete documentation index at: https://docs.chargefy.io/llms.txt
Use this file to discover all available pages before exploring further.
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
import { Chargefy } from '@chargefy/sdk'
export const chargefy = new Chargefy ({
accessToken: process . env . CHARGEFY_ACCESS_TOKEN ! ,
// Para sandbox:
// server: 'sandbox'
})
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"
}
]
}'
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: 'admin@empresa.com' ,
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": "admin@empresa.com",
"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)
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.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.