> ## 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.

# Precificação por Assento

> Guia passo a passo para implementar precificacao baseada em numero de usuarios (assentos) com a Chargefy

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

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

    Admin->>SeuApp: Escolher plano com 5 assentos
    SeuApp->>Chargefy: Criar checkout (quantidade: 5)
    Admin->>Chargefy: Pagar R$ 249,50 (5 x R$ 49,90)
    Chargefy->>Processador: Criar recorrencia
    Processador-->>Chargefy: Plano ativo
    Note over SeuApp: Meses depois...
    Admin->>SeuApp: Adicionar 3 assentos
    SeuApp->>Chargefy: Atualizar assinatura (quantidade: 8)
    Chargefy->>Processador: Atualizar plano com prorata
    Chargefy-->>SeuApp: webhook subscription.updated
```

## 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 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

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

```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 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

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

```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_equipe_mensal_xxxx",
    "quantity": 5,
    "customerEmail": "admin@empresa.com",
    "successUrl": "https://meuapp.com.br/assinatura/sucesso",
    "metadata": {
      "organizationId": "org_123"
    }
  }'
```

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

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

```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 '{
    "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
```

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

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

```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 '{
    "quantity": 6,
    "proration": false
  }'
```

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

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

```tsx src/components/SeatManagement.tsx theme={null}
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

> [card] [Variantes de Produto](https://scaleup-c34c4386.mintlify.site/guides/create-variants.md)
>
> Combine assentos com diferentes niveis de plano.

> [card] [Upgrade de Assinatura](https://scaleup-c34c4386.mintlify.site/guides/subscription-upgrades.md)
>
> Permita upgrades de plano alem de adicionar assentos.


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