Skip to content

Plans API — Planos de Assinatura

Base Path: /api/v1/plans


Endpoints

Metodo Endpoint Descricao Auth
GET /plans Listar planos ativos (publico) Nao
GET /plans/admin Listar todos os planos (admin) Admin
POST /plans Criar plano (admin) Admin
PUT /plans Atualizar plano (admin) Admin
POST /plans/{plan_id}/inactivate Inativar plano (admin) Admin
POST /plans/{plan_id}/activate Reativar plano (admin) Admin
POST /plans/{plan_id}/sync-stripe Sincronizar com Stripe (admin) Admin
DELETE /plans Deletar plano (admin) Admin

GET /plans (PUBLICO)

Listar planos ativos ordenados por display_order.

Uso: Pagina de pricing/signup do frontend.

Request

GET /api/v1/plans

Nao requer autenticacao.

Filtro por Gateway

O parametro gateway filtra planos por gateway de pagamento:

# Todos os planos sincronizados (default)
GET /api/v1/plans

# Apenas planos com Stripe configurado
GET /api/v1/plans?gateway=stripe

# Apenas planos com Asaas configurado
GET /api/v1/plans?gateway=asaas
Valor Comportamento
(omitido) Retorna planos sincronizados com pelo menos um gateway (stripe_price_id != null OR asaas_plan_id != null)
stripe Apenas planos com stripe_price_id preenchido
asaas Apenas planos com asaas_plan_id preenchido

Quando usar o filtro

Se o frontend oferece ambos os gateways, nao use filtro — o endpoint retorna todos os planos sincronizados. Se o frontend quer exibir apenas planos disponiveis para Stripe, use ?gateway=stripe.

Response (200 OK)

{
  "docs": [
    {
      "_id": "507f1f77bcf86cd799439011",
      "plan_id": "plan-basic",
      "stripe_product_id": "prod_RU4xxxxxxx",
      "stripe_price_id": "price_1Rxxxxxxxx",
      "name": "Plano Basico",
      "description": "Acesso completo ao catalogo de videos com qualidade HD",
      "amount": 2990,
      "currency": "BRL",
      "interval_unit": "MONTH",
      "interval_length": 1,
      "trial_days": 7,
      "trial_enabled": true,
      "features": [
        "Qualidade HD (720p)",
        "1 tela simultanea",
        "Catalogo completo",
        "Sem anuncios",
        "7 dias gratis"
      ],
      "max_devices": 1,
      "tier": 1,
      "is_active": true,
      "display_order": 1
    }
  ],
  "pagination": {
    "current_page": 0,
    "qty_docs_page": 3,
    "qty_total_docs": 3,
    "qty_of_pages": 1
  },
  "links": {},
  "msg": null
}

Campos do Plano

Campo Tipo Descricao
plan_id string ID interno (slug: "plan-basic")
stripe_product_id string \| null ID do Stripe Product (prod_xxx) — null se nao sincronizado
stripe_price_id string \| null ID do Stripe Price (price_xxx) — null se nao sincronizado
asaas_plan_id string \| null ID do plano no Asaas — null se nao sincronizado. Read-only (populado pelo backend via sync)
name string Nome do plano
description string Descricao detalhada
amount int Valor em centavos (2990 = R$ 29,90)
currency string Codigo da moeda (BRL)
interval_unit string Frequencia: MONTH, YEAR, WEEK, DAY
interval_length int Intervalo (1 = mensal)
trial_days int \| null Dias de trial gratuito
trial_enabled bool Se trial esta ativo
features string[] Lista de funcionalidades do plano
max_devices int \| null Max telas simultaneas
tier int Nivel do plano (0-3)
is_active bool Se esta disponivel para compra
display_order int Ordem de exibicao no frontend

Tier (Nivel do Plano)

O campo tier controla o acesso a videos restritos:

Tier Nome Descricao
0 Universal Acesso apenas a videos gratuitos
1 Basico Plano Basico ou superior
2 Plus Plano Plus ou superior
3 Premium Apenas Plano Premium

Admin Endpoints

POST /plans — Criar Plano

Criar novo plano e publicar nos gateways selecionados.

POST /api/v1/plans
Authorization: Bearer eyJhbGci... (admin token)
Content-Type: application/json

[{
  "plan_id": "plan-enterprise",
  "name": "Plano Enterprise",
  "description": "Plano para empresas com recursos avancados",
  "amount": 19900,
  "currency": "BRL",
  "interval_unit": "MONTH",
  "interval_length": 1,
  "trial_days": 30,
  "trial_enabled": true,
  "features": [
    "4K + HDR",
    "10 telas simultaneas",
    "API de integracao",
    "Suporte prioritario"
  ],
  "max_devices": 10,
  "tier": 3,
  "publish_to": ["asaas", "stripe"],
  "is_active": true,
  "display_order": 4
}]

Campo publish_to

O campo publish_to define em quais gateways o plano sera criado:

Valor Efeito
["asaas"] Cria apenas no Asaas
["stripe"] Cria Product + Price no Stripe
["asaas", "stripe"] Cria em ambos os gateways

Comportamento por gateway:

  • Asaas: Cria o plano na API Asaas e salva asaas_plan_id
  • Stripe: Cria um Product (identidade do plano) + Price (config de cobranca recorrente) e salva stripe_product_id + stripe_price_id

Stripe: Product vs Price

No Stripe, um plano corresponde a dois objetos: Product (nome, descricao) e Price (valor, intervalo, moeda). O Price e imutavel — se o valor mudar, um novo Price e criado e o antigo arquivado automaticamente.

Response (200 OK)

Retorna o plano criado com os IDs dos gateways preenchidos.

Errors

HTTP Causa
400 Dados invalidos ou ausentes
409 plan_id ja existe
502 Falha ao criar no Asaas ou Stripe

PUT /plans — Atualizar Plano

Atualizar plano existente. Sincroniza automaticamente com os gateways configurados.

PUT /api/v1/plans
Authorization: Bearer eyJhbGci... (admin token)
Content-Type: application/json

[{
  "_id": "507f1f77bcf86cd799439011",
  "name": "Plano Basico Atualizado",
  "amount": 3490,
  "features": [
    "Qualidade HD (720p)",
    "1 tela simultanea",
    "Catalogo completo",
    "Sem anuncios",
    "10 dias gratis"
  ]
}]

Sincronizacao automatica:

  • Stripe Product: Atualiza name e description
  • Stripe Price: Se amount mudou, cria novo Price e arquiva o antigo automaticamente

Mudanca de preco no Stripe

Ao alterar o amount, o sistema automaticamente:

  1. Cria um novo Stripe Price com o valor atualizado
  2. Arquiva o Price antigo (active=false)
  3. Atualiza stripe_price_id no MongoDB

Subscriptions existentes continuam com o preco antigo. Novas subscriptions usam o novo preco.

POST /plans/{plan_id}/inactivate — Inativar Plano

Inativar plano (nao aceita novas subscriptions, existentes continuam).

POST /api/v1/plans/plan-basic/inactivate
Authorization: Bearer eyJhbGci...

Efeitos por gateway:

  • MongoDB: is_active = false
  • Stripe: Arquiva o Price (active=false) e desativa o Product

POST /plans/{plan_id}/activate — Reativar Plano

Reativar plano previamente inativado.

POST /api/v1/plans/plan-basic/activate
Authorization: Bearer eyJhbGci...

Efeitos por gateway:

  • MongoDB: is_active = true
  • Stripe: Reativa o Product (active=true)

POST /plans/{plan_id}/sync-stripe — Sincronizar com Stripe

Para planos criados sem Stripe, sincroniza posteriormente. Cria Product + Price no Stripe.

POST /api/v1/plans/plan-basic/sync-stripe
Authorization: Bearer eyJhbGci...

Response (200 OK):

{
  "message": "Plan \"plan-basic\" synced to Stripe successfully.",
  "plan_id": "plan-basic",
  "stripe_product_id": "prod_RU4xxxxxxx",
  "stripe_price_id": "price_1Rxxxxxxxx"
}

Erro 409 se o plano ja tem stripe_price_id.

Caso de uso

Planos existentes criados sem Stripe podem ser sincronizados via este endpoint, sem precisar recriar o plano.

GET /plans/admin — Listar Todos (Admin)

Listar todos os planos (incluindo inativos e nao sincronizados).

GET /api/v1/plans/admin?query={"is_active":false}
Authorization: Bearer eyJhbGci... (admin token)

Suporta filtros MongoDB via query parameter.

DELETE /plans — Deletar Plano

Deletar plano permanentemente.

Atencao

Prefira usar POST /plans/{plan_id}/inactivate ao inves de deletar. Delecao e permanente e nao cancela subscriptions existentes.

DELETE /api/v1/plans?_id=507f1f77bcf86cd799439011
Authorization: Bearer eyJhbGci... (admin token)

Exemplo de UI: Pagina de Pricing

Exemplo Flutter/Dart:

import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:flutter/material.dart';

class PricingPage extends StatefulWidget {
  @override
  _PricingPageState createState() => _PricingPageState();
}

class _PricingPageState extends State<PricingPage> {
  List<Map<String, dynamic>> plans = [];

  @override
  void initState() {
    super.initState();
    _loadPlans();
  }

  Future<void> _loadPlans() async {
    // Sem filtro: retorna planos sincronizados com qualquer gateway
    // Com filtro: GET /plans?gateway=stripe (apenas Stripe)
    final response = await http.get(
      Uri.parse('${AppConfig.baseUrl}/plans'),
    );
    if (response.statusCode == 200) {
      final data = jsonDecode(response.body);
      setState(() => plans = List<Map<String, dynamic>>.from(data['docs']));
    }
  }

  /// Verifica se um plano suporta Stripe
  bool _hasStripe(Map<String, dynamic> plan) {
    return plan['stripe_price_id'] != null;
  }

  /// Verifica se um plano suporta Asaas
  bool _hasAsaas(Map<String, dynamic> plan) {
    return plan['asaas_plan_id'] != null;
  }

  void _selectPlan(String planId, String gateway) {
    Navigator.pushNamed(
      context,
      '/subscribe',
      arguments: {'planId': planId, 'gateway': gateway},
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Planos')),
      body: ListView.builder(
        padding: EdgeInsets.all(16),
        itemCount: plans.length,
        itemBuilder: (context, index) {
          final plan = plans[index];
          final features = List<String>.from(plan['features'] ?? []);
          return Card(
            margin: EdgeInsets.only(bottom: 16),
            child: Padding(
              padding: EdgeInsets.all(16),
              child: Column(
                children: [
                  Text(plan['name'], style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
                  SizedBox(height: 8),
                  Text(
                    'R\$ ${(plan['amount'] / 100).toStringAsFixed(2)}/mes',
                    style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
                  ),
                  SizedBox(height: 12),
                  ...features.map((f) => Padding(
                    padding: EdgeInsets.symmetric(vertical: 2),
                    child: Text(f),
                  )),
                  SizedBox(height: 16),
                  // Botoes por gateway disponivel
                  if (_hasStripe(plan))
                    ElevatedButton(
                      onPressed: () => _selectPlan(plan['plan_id'], 'stripe'),
                      child: Text('Assinar com Cartao (Stripe)'),
                    ),
                  if (_hasAsaas(plan))
                    OutlinedButton(
                      onPressed: () => _selectPlan(plan['plan_id'], 'asaas'),
                      child: Text('Assinar com Asaas'),
                    ),
                  if (plan['trial_enabled'] == true)
                    Padding(
                      padding: EdgeInsets.only(top: 8),
                      child: Text(
                        '${plan['trial_days']} dias gratis',
                        style: TextStyle(color: Colors.green),
                      ),
                    ),
                ],
              ),
            ),
          );
        },
      ),
    );
  }
}

Workflow Frontend

graph TD
    A[Usuario visita /pricing] --> B["GET /api/v1/plans"]
    B --> C[Exibir cards de planos]
    C --> D[Usuario clica em Assinar]
    D --> E{Qual gateway?}
    E -->|Stripe| F["POST /api/v1/stripe/subscribe"]
    E -->|Asaas| G["POST /api/v1/asaas/subscribe"]

Notas de Implementacao

Modelo Multi-Gateway

Cada plano pode estar sincronizado com um ou mais gateways:

Cenario stripe_price_id asaas_plan_id Visivel em GET /plans?
Criado com publish_to: ["stripe"] price_xxx null Sim
Criado com publish_to: ["asaas"] null asaas_xxx Sim
Criado com publish_to: ["asaas", "stripe"] price_xxx asaas_xxx Sim
Criado sem sync (rascunho) null null Nao

Indices MongoDB

  • plan_id (unico)
  • is_active
  • is_active + display_order (composto, para listagem)
  • legacy_id

Ver Tambem