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:
- Cancela a assinatura antiga no gateway
- Cria uma nova assinatura com o mesmo plano e novo cartao
- Atualiza o
current_subscription_iddo 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.
| 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/subscriptionantes de montar o form - [ ] Formulario Asaas: 11 campos (6 cartao + 5 titular) com validacao client-side
- [ ] Formulario Stripe:
CardFielddo SDK βcreatePaymentMethodβpm_xxx - [ ] Chamar
PUT /api/v1/customers/me/subscription/renewcom payload correto - [ ] Tratar resposta: exibir sucesso ("Cartao atualizado!") e atualizar estado local
- [ ] Tratar erros: exibir mensagem do campo
detailda 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βactivevia 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-cardouPUT /stripe/subscription/payment-method): atualiza in-place sem cancelar. Use para troca preventiva de cartao com subscription saudavel.