Skip to content

Renew Subscription β€” Trocar Cartao de Credito

Guia completo para o frontend implementar a troca de cartao de credito em uma assinatura ativa.


Visao Geral

O endpoint PUT /api/v1/customers/me/subscription/renew permite que o customer troque o cartao de credito da assinatura ativa sem perder o plano.

Internamente, a API:

  1. Cancela a assinatura antiga no gateway
  2. Cria uma nova assinatura com o mesmo plano e novo cartao
  3. Atualiza o current_subscription_id do customer

O gateway e detectado automaticamente a partir da assinatura atual β€” o frontend nao precisa informar.


Fluxo Completo (ASCII)

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                        FLUTTER APP                                  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                               β”‚
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚ GET /customers/me/  β”‚
                    β”‚   subscription      β”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                               β”‚
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚ Verifica gateway    β”‚
                    β”‚ da subscription     β”‚
                    β”‚ ativa               β”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                               β”‚
              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
              β”‚                                 β”‚
     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”
     β”‚  gateway: asaas β”‚              β”‚ gateway: stripe β”‚
     β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜              β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              β”‚                                 β”‚
     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”
     β”‚ Coletar dados   β”‚              β”‚ Criar PM via    β”‚
     β”‚ do cartao       β”‚              β”‚ Stripe SDK      β”‚
     β”‚ (form manual)   β”‚              β”‚ (tokenizado)    β”‚
     β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜              β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              β”‚                                 β”‚
     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
     β”‚ PUT /me/subscription β”‚         β”‚ PUT /me/subscription β”‚
     β”‚   /renew             β”‚         β”‚   /renew             β”‚
     β”‚                      β”‚         β”‚                      β”‚
     β”‚ body: {              β”‚         β”‚ body: {              β”‚
     β”‚   credit_card,       β”‚         β”‚   payment_method_id  β”‚
     β”‚   credit_card_       β”‚         β”‚ }                    β”‚
     β”‚     holder_info      β”‚         β”‚                      β”‚
     β”‚ }                    β”‚         β”‚                      β”‚
     β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              β”‚                                β”‚
              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                               β”‚
              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
              β”‚           API BACKEND           β”‚
              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                               β”‚
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚ 1. Buscar customer  β”‚
                    β”‚    + subscription   β”‚
                    β”‚    ativa            β”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                               β”‚
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚ 2. Validar status   β”‚
                    β”‚    (active/pending) β”‚
                    β”‚    + dados do card  β”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                               β”‚
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚ 3. Cancelar sub     β”‚
                    β”‚    antiga no        β”‚
                    β”‚    gateway          β”‚
                    β”‚    (best-effort)    β”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                               β”‚
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚ 4. Marcar antiga    β”‚
                    β”‚    como "cancelled" β”‚
                    β”‚    no MongoDB       β”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                               β”‚
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚ 5. Criar nova sub   β”‚
                    β”‚    no gateway com   β”‚
                    β”‚    mesmo plano +    β”‚
                    β”‚    novo cartao      β”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                               β”‚
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚ 6. Inserir nova     β”‚
                    β”‚    sub no MongoDB   β”‚
                    β”‚    (status=pending) β”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                               β”‚
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚ 7. Atualizar        β”‚
                    β”‚    customer.current β”‚
                    β”‚    _subscription_id β”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                               β”‚
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚ 8. Retornar nova    β”‚
                    β”‚    subscription     β”‚
                    β”‚    (200 OK)         β”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                               β”‚
              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
              β”‚ Webhook PAYMENT_RECEIVED ativa  β”‚
              │ a subscription (pending→active) │
              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Endpoint

PUT /api/v1/customers/me/subscription/renew
Authorization: Bearer <jwt_token>
Content-Type: application/json

Payloads por Gateway

Asaas (gateway detectado automaticamente)

O frontend deve coletar os dados do cartao manualmente (Asaas nao tem SDK client-side).

{
  "credit_card": {
    "holderName": "NOME NO CARTAO",
    "number": "5162306219378829",
    "expiryMonth": "05",
    "expiryYear": "2028",
    "ccv": "318"
  },
  "credit_card_holder_info": {
    "name": "Nome Completo",
    "email": "email@example.com",
    "cpfCnpj": "24971563792",
    "postalCode": "89223005",
    "addressNumber": "277",
    "phone": "47998781877"
  }
}
Campo Tipo Obrigatorio Descricao
credit_card.holderName String Sim Nome como impresso no cartao (uppercase)
credit_card.number String Sim Numero do cartao (sem espacos)
credit_card.expiryMonth String Sim Mes de expiracao (2 digitos, ex: "05")
credit_card.expiryYear String Sim Ano de expiracao (4 digitos, ex: "2028")
credit_card.ccv String Sim Codigo de seguranca (3-4 digitos)
credit_card_holder_info.name String Sim Nome completo do titular
credit_card_holder_info.email String Sim Email do titular
credit_card_holder_info.cpfCnpj String Sim CPF ou CNPJ (somente numeros)
credit_card_holder_info.postalCode String Sim CEP (8 digitos, sem traco)
credit_card_holder_info.addressNumber String Sim Numero do endereco
credit_card_holder_info.phone String Sim Telefone com DDD (somente numeros)

Stripe (gateway detectado automaticamente)

O frontend deve usar o Stripe SDK para tokenizar o cartao e obter o payment_method_id.

{
  "payment_method_id": "pm_1234567890abcdef"
}
Campo Tipo Obrigatorio Descricao
payment_method_id String Sim Token gerado pelo Stripe SDK (pm_xxx)

Resposta (200 OK)

{
  "subscription": {
    "id": "507f1f77bcf86cd799439011",
    "plan_id": "plan-basic",
    "gateway": "asaas",
    "status": "pending",
    "payment_method": "credit_card",
    "started_at": "2026-04-03T12:00:00+00:00"
  },
  "message": "Assinatura renovada com sucesso. Novo cartao configurado."
}

Status inicial: pending

A nova assinatura sempre inicia com status: "pending". O webhook do gateway (PAYMENT_RECEIVED no Asaas, invoice.paid no Stripe) ativara a assinatura automaticamente (pending β†’ active).


Erros

Status Detalhe Causa
400 credit_card and credit_card_holder_info are required for Asaas gateway. Gateway e Asaas mas campos de cartao ausentes
400 payment_method_id is required for Stripe gateway. Gateway e Stripe mas token ausente
400 Cannot renew subscription with status "cancelled". Subscription nao esta active/pending
400 Plan is no longer active Plano foi desativado
404 No active subscription found Customer nao tem current_subscription_id
404 Subscription not found ID existe mas documento nao
404 Customer not found JWT valido mas customer nao existe

Tutorial Flutter β€” Implementacao Passo a Passo

1. Obter a subscription atual (detectar gateway)

Future<Map<String, dynamic>> getSubscription(String token) async {
  final response = await http.get(
    Uri.parse('$baseUrl/api/v1/customers/me/subscription'),
    headers: {'Authorization': 'Bearer $token'},
  );
  if (response.statusCode != 200) {
    throw Exception('Falha ao buscar assinatura');
  }
  return jsonDecode(response.body);
}

O campo gateway na resposta ("asaas" ou "stripe") determina qual payload enviar.

2. Montar o payload por gateway

Map<String, dynamic> buildRenewPayload({
  required String gateway,
  // Asaas fields
  String? holderName,
  String? cardNumber,
  String? expiryMonth,
  String? expiryYear,
  String? ccv,
  String? name,
  String? email,
  String? cpfCnpj,
  String? postalCode,
  String? addressNumber,
  String? phone,
  // Stripe fields
  String? paymentMethodId,
}) {
  if (gateway == 'asaas') {
    return {
      'credit_card': {
        'holderName': holderName!.toUpperCase(),
        'number': cardNumber!.replaceAll(' ', ''),
        'expiryMonth': expiryMonth!.padLeft(2, '0'),
        'expiryYear': expiryYear!,
        'ccv': ccv!,
      },
      'credit_card_holder_info': {
        'name': name!,
        'email': email!,
        'cpfCnpj': cpfCnpj!.replaceAll(RegExp(r'[.\-/]'), ''),
        'postalCode': postalCode!.replaceAll('-', ''),
        'addressNumber': addressNumber!,
        'phone': phone!.replaceAll(RegExp(r'[^0-9]'), ''),
      },
    };
  } else {
    // Stripe β€” payment_method_id vem do Stripe SDK
    return {
      'payment_method_id': paymentMethodId!,
    };
  }
}

3. Chamar o endpoint

Future<Map<String, dynamic>> renewSubscription({
  required String token,
  required String gateway,
  // ... card fields
}) async {
  final payload = buildRenewPayload(
    gateway: gateway,
    // ... pass fields
  );

  final response = await http.put(
    Uri.parse('$baseUrl/api/v1/customers/me/subscription/renew'),
    headers: {
      'Authorization': 'Bearer $token',
      'Content-Type': 'application/json',
    },
    body: jsonEncode(payload),
  );

  if (response.statusCode != 200) {
    final error = jsonDecode(response.body);
    throw Exception(error['detail'] ?? 'Falha ao renovar assinatura');
  }

  return jsonDecode(response.body);
}

4. Tratar a resposta na UI

void handleRenewResult(Map<String, dynamic> result) {
  final subscription = result['subscription'];
  final status = subscription['status']; // sempre "pending" inicialmente

  if (status == 'pending') {
    // Mostrar mensagem: "Cartao atualizado! Aguardando confirmacao do pagamento."
    // O webhook ativara a assinatura em segundos (cartao de credito)
    showSnackBar('Cartao atualizado com sucesso!');
  }

  // Atualizar estado local da subscription
  setState(() {
    currentSubscription = subscription;
  });
}

5. Fluxo completo na tela

class RenewSubscriptionScreen extends StatefulWidget {
  @override
  _RenewSubscriptionScreenState createState() =>
      _RenewSubscriptionScreenState();
}

class _RenewSubscriptionScreenState extends State<RenewSubscriptionScreen> {
  String? gateway;
  bool isLoading = false;

  // Asaas form controllers
  final holderNameCtrl = TextEditingController();
  final cardNumberCtrl = TextEditingController();
  final expiryMonthCtrl = TextEditingController();
  final expiryYearCtrl = TextEditingController();
  final ccvCtrl = TextEditingController();
  final nameCtrl = TextEditingController();
  final emailCtrl = TextEditingController();
  final cpfCtrl = TextEditingController();
  final postalCodeCtrl = TextEditingController();
  final addressNumberCtrl = TextEditingController();
  final phoneCtrl = TextEditingController();

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

  Future<void> _loadGateway() async {
    final sub = await getSubscription(token);
    setState(() {
      // Gateway vem dentro de docs[0] ou do campo direto
      gateway = sub['docs']?[0]?['gateway'] ?? sub['gateway'];
    });
  }

  Future<void> _onRenew() async {
    setState(() => isLoading = true);

    try {
      Map<String, dynamic> result;

      if (gateway == 'stripe') {
        // 1. Tokenizar cartao via Stripe SDK
        final paymentMethod = await Stripe.instance.createPaymentMethod(
          params: PaymentMethodParams.card(
            paymentMethodData: PaymentMethodData(),
          ),
        );

        // 2. Chamar renew com o token
        result = await renewSubscription(
          token: token,
          gateway: 'stripe',
          paymentMethodId: paymentMethod.id,
        );
      } else {
        // Asaas β€” enviar dados coletados do form
        result = await renewSubscription(
          token: token,
          gateway: 'asaas',
          holderName: holderNameCtrl.text,
          cardNumber: cardNumberCtrl.text,
          expiryMonth: expiryMonthCtrl.text,
          expiryYear: expiryYearCtrl.text,
          ccv: ccvCtrl.text,
          name: nameCtrl.text,
          email: emailCtrl.text,
          cpfCnpj: cpfCtrl.text,
          postalCode: postalCodeCtrl.text,
          addressNumber: addressNumberCtrl.text,
          phone: phoneCtrl.text,
        );
      }

      handleRenewResult(result);
    } catch (e) {
      showSnackBar('Erro: ${e.toString()}');
    } finally {
      setState(() => isLoading = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    if (gateway == null) return CircularProgressIndicator();

    return Scaffold(
      appBar: AppBar(title: Text('Trocar Cartao')),
      body: SingleChildScrollView(
        padding: EdgeInsets.all(16),
        child: Column(
          children: [
            if (gateway == 'asaas') ...[
              // Formulario Asaas: campos do cartao + dados do titular
              TextField(controller: holderNameCtrl, decoration: InputDecoration(labelText: 'Nome no cartao')),
              TextField(controller: cardNumberCtrl, decoration: InputDecoration(labelText: 'Numero do cartao')),
              Row(children: [
                Expanded(child: TextField(controller: expiryMonthCtrl, decoration: InputDecoration(labelText: 'Mes'))),
                SizedBox(width: 8),
                Expanded(child: TextField(controller: expiryYearCtrl, decoration: InputDecoration(labelText: 'Ano'))),
                SizedBox(width: 8),
                Expanded(child: TextField(controller: ccvCtrl, decoration: InputDecoration(labelText: 'CVV'))),
              ]),
              Divider(height: 32),
              TextField(controller: nameCtrl, decoration: InputDecoration(labelText: 'Nome completo')),
              TextField(controller: emailCtrl, decoration: InputDecoration(labelText: 'Email')),
              TextField(controller: cpfCtrl, decoration: InputDecoration(labelText: 'CPF')),
              TextField(controller: postalCodeCtrl, decoration: InputDecoration(labelText: 'CEP')),
              TextField(controller: addressNumberCtrl, decoration: InputDecoration(labelText: 'Numero')),
              TextField(controller: phoneCtrl, decoration: InputDecoration(labelText: 'Telefone')),
            ] else ...[
              // Stripe: usar CardField do SDK
              // CardField(onCardChanged: (card) { ... }),
              Text('Insira os dados do cartao abaixo:'),
              // Stripe CardField widget aqui
            ],
            SizedBox(height: 24),
            ElevatedButton(
              onPressed: isLoading ? null : _onRenew,
              child: isLoading
                  ? CircularProgressIndicator()
                  : Text('Trocar Cartao'),
            ),
          ],
        ),
      ),
    );
  }
}

Checklist de Implementacao

  • [ ] Tela de "Trocar Cartao" acessivel a partir da tela de assinatura
  • [ ] Detectar gateway via GET /customers/me/subscription antes de montar o form
  • [ ] Formulario Asaas: 11 campos (6 cartao + 5 titular) com validacao client-side
  • [ ] Formulario Stripe: CardField do SDK β†’ createPaymentMethod β†’ pm_xxx
  • [ ] Chamar PUT /api/v1/customers/me/subscription/renew com payload correto
  • [ ] Tratar resposta: exibir sucesso ("Cartao atualizado!") e atualizar estado local
  • [ ] Tratar erros: exibir mensagem do campo detail da resposta
  • [ ] Testar com cartao de teste Asaas: 5162306219378829 (aprovado em sandbox)
  • [ ] Testar com cartao de teste Stripe: 4242424242424242
  • [ ] Verificar que a assinatura muda de pending β†’ active via webhook (segundos)

Rotas Relacionadas

Rota Metodo Descricao
GET /customers/me/subscription GET Obter assinatura ativa (detectar gateway)
PUT /customers/me/subscription/renew PUT Trocar cartao (este doc)
PUT /asaas/subscription/credit-card PUT Atualizar cartao Asaas (sem cancelar)
PUT /stripe/subscription/payment-method PUT Atualizar cartao Stripe (sem cancelar)
DELETE /customers/me/subscription DELETE Cancelar assinatura

Quando usar renew vs update card

  • Renew (PUT /me/subscription/renew): cancela e recria. Use quando o cartao anterior ja expirou ou foi recusado e a subscription esta em estado problematico.
  • Update card (PUT /asaas/subscription/credit-card ou PUT /stripe/subscription/payment-method): atualiza in-place sem cancelar. Use para troca preventiva de cartao com subscription saudavel.