Gerenciar Pagamento & Recorrência (Front-end)¶
Guia de referência para as telas de "Minha assinatura": como o cliente vê a recorrência, troca o método de pagamento, troca o cartão, retoma um PIX pendente, cancela e volta a assinar.
Todos os endpoints aqui são do próprio cliente (Bearer <jwt> do customer).
Nada nesta página exige permissão de admin.
1. Modelo mental (leia antes de desenhar a tela)¶
- O cliente tem no máximo uma assinatura corrente, apontada por
customer.current_subscription_id. Cancelar zera esse ponteiro. - A assinatura carrega dois eixos independentes:
gateway:asaas|stripe— quem cobra;payment_method:credit_card|pix— como cobra.
- O front não escolhe o gateway em nenhuma troca: ele é herdado da assinatura
atual (nas rotas de troca) ou do padrão da plataforma
(
GET /config/payment-gateway) num novo assinar. - Trocar cartão ≠ trocar método. São caminhos diferentes:
| Intenção do cliente | Caminho | Efeito na recorrência |
|---|---|---|
| "Meu cartão venceu / quero outro cartão" | PUT /asaas/subscription/credit-card ou PUT /stripe/subscription/payment-method |
In-place — mesma assinatura, mesmo ciclo, sem nova cobrança |
| "Quero trocar cartão → PIX" (ou voltar a assinar por PIX) | PUT /customers/me/subscription/renew com payment_method: "pix" |
Cancela e recria — devolve QR, novo período |
| "Quero trocar de plano" | PUT /customers/me/subscription/renew com plan_id |
Cancela e recria no plano novo |
| "Quero cancelar" | DELETE /customers/me/subscription |
Encerra no gateway + local |
| "Quero assinar de novo" (sem assinatura corrente) | POST /asaas/subscribe (cartão) ou POST /asaas/subscribe/pix-automatico (PIX) |
Cria assinatura nova |
POST /customers/me/subscribe não funciona
A rota existe mas responde 501 (Subscription gateway not yet configured).
Para assinar, use as rotas de gateway (/asaas/subscribe*, /stripe/subscribe*).
2. Tela "Minha assinatura" — o que carregar¶
GET /api/v1/customers/me/subscription → estado da recorrência
GET /api/v1/config/payment-gateway → { "default_payment_gateway": "asaas" }
GET /api/v1/config/pix-subscription-mode → { "pix_subscription_mode": "recurring" }
GET /api/v1/config/pix-billing-mode → { "pix_billing_mode": "monthly" }
GET /api/v1/plans → planos ativos (troca de plano)
GET /customers/me/subscription — 200¶
{
"subscription": {
"_id": "507f1f77bcf86cd799439011",
"plan_id": "plan-basic",
"gateway": "asaas",
"status": "active",
"payment_method": "pix",
"asaas_pix_auth_id": "pixauth_xxx",
"pix_period_months": 1,
"started_at": "2026-08-01T12:00:00+00:00",
"next_billing_date": null,
"expires_at": "2026-09-01T12:00:00+00:00"
},
"message": "Active subscription retrieved successfully"
}
Campos que a tela deve usar:
| Campo | Como interpretar na UI |
|---|---|
status |
Já vem efetivo: um active vencido chega como expired, e com falhas de pagamento demais chega como suspended. Não recalcule no app |
payment_method |
pix → assinatura por PIX; credit_card → cartão |
asaas_pix_auth_id |
Presente ⇒ Pix Automático (débito recorrente autorizado no banco). Ausente com payment_method: 'pix' ⇒ PIX manual (QR por ciclo, legado) |
pix_period_months |
Meses vendidos por ciclo PIX (12 = anual). Congelado no assinar — não reflita a config atual do painel |
expires_at |
Fim do acesso pago (PIX). Use para "Sua assinatura vale até …" |
next_billing_date |
Próxima cobrança do cartão. null em PIX — nesse caso mostre expires_at |
pix_qr_expired |
Só aparece quando status: 'pending' + payment_method: 'pix'. true ⇒ o QR atual já passou da janela de 10 min: ofereça "Gerar novo QR" |
Stripe pendente resolve sozinho
Para assinatura Stripe em pending, este GET consulta a Stripe ao vivo e
grava o status real. Um pending que virou incomplete/canceled lá volta
daqui como cancelled — o polling do app tem resposta definitiva, não fica
eternamente pendente.
404 No active subscription found for this customer é o caso normal de "cliente
sem assinatura" — trate como estado da tela (mostrar planos), não como erro.
3. Trocar o cartão (mantendo a recorrência)¶
Use quando o cliente só quer atualizar o cartão. É in-place: a assinatura, o ciclo e a data da próxima cobrança não mudam, e nenhuma cobrança é disparada.
{
"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"
}
}
O pm_xxx vem do SDK da Stripe no cliente — o app nunca envia número de cartão
para esta rota.
Erros comuns
| Status | Detalhe | Leitura |
|---|---|---|
| 404 | No active subscription |
Sem assinatura corrente — não ofereça a ação |
| 400 | Current subscription is not managed by Asaas./Stripe. |
Chamou a rota do gateway errado — decida pelo gateway do GET |
| 400 | Subscription has no Asaas ID / no Stripe ID |
Assinatura sem objeto de recorrência no gateway (ex.: Pix Automático) — não é caminho de cartão; use renew |
Assinatura PIX não tem cartão para trocar
Numa assinatura PIX (inclusive Pix Automático) estas rotas falham com 400.
A troca de método passa obrigatoriamente pelo renew (§4).
4. Trocar o método de pagamento / trocar de plano (renew)¶
Este endpoint cancela a assinatura atual e cria uma nova. Gateway é detectado automaticamente a partir da assinatura vigente.
| Campo | Tipo | Quando enviar |
|---|---|---|
payment_method |
"pix" | "credit_card" |
Opcional. Omitido = herda o método da assinatura anterior |
plan_id |
String | Opcional. Omitido = mantém o plano atual |
promo_code |
String | Opcional, aplicado só na renovação PIX |
credit_card + credit_card_holder_info |
Objetos | Obrigatórios quando o caminho é cartão no Asaas |
payment_method_id |
String (pm_xxx) |
Obrigatório quando o caminho é cartão na Stripe |
4.1 A regra que mais confunde: PIX ganha do cartão¶
O backend renova via PIX recorrente quando:
- o payload traz
payment_method: "pix", ou - a assinatura anterior era PIX (
payment_method == 'pix'ou temasaas_pix_auth_id).
Ou seja: para um cliente que veio de PIX, mandar payment_method: "credit_card" é
no-op — ele recebe um QR de volta, não uma assinatura de cartão. Motivo: a tela
de renovação criptografa cartão com a chave RSA da PagSeguro, gateway não
integrado neste backend; uma renovação por cartão para esse cliente não teria como
ser honrada no servidor.
Nunca roteie um fluxo de cartão por aqui esperando cartão
Sempre inspecione a resposta: se vier info.pix.qr_code, a tela precisa
renderizar QR, não "cartão atualizado com sucesso".
4.2 Caminho PIX (Pix Automático)¶
Só é permitido em assinatura não-ativa. Com assinatura ainda ativa a API responde 400:
Subscription is still active. A PIX renewal would revoke the current authorization and charge a new full period without proration. Cancel the subscription first, or renew after it expires.
A razão é concreta: renovar por PIX revoga a autorização vigente e emite um QR de um período inteiro, sem pro-rata — o cliente pagaria o período duas vezes. Na UI, para quem está ativo, ofereça cancelar e depois assinar por PIX (ou esperar expirar), nunca "trocar para PIX agora".
Requisição
Resposta 200 (mesmo envelope do subscribe PIX — QR em info.pix)
{
"info": {
"pix": {
"authorization_id": "pixauth_xxx",
"qr_code": "00020101...6304ABCD",
"qr_code_image": "iVBORw0KGgoAAAANS...",
"generated_at": "2026-08-19T12:00:00+00:00",
"expiration_date": "2026-08-19T12:10:00+00:00",
"value": 19.9,
"recurring_value": 39.9
},
"asaas_pix_auth_id": "pixauth_xxx",
"authorization_status": "PENDING_FIRST_PAYMENT",
"message": "Autorização Pix Automático criada. Escaneie o QR code e confirme a recorrência no app do seu banco."
},
"docs": [ { "...": "documento da subscription (status pending)" } ]
}
value= primeira cobrança (pode estar com desconto de cupom);recurring_value= valor cheio das cobranças seguintes.expiration_dateé a janela de 10 minutos da FDPlay (ADR-0095), não o TTL da Asaas. Mostre contagem regressiva por ela.- Um único QR paga a 1ª cobrança e autoriza a recorrência no banco. Depois disso a Asaas gera as cobranças sozinha — não há QR por ciclo.
4.3 Caminho cartão (mesmo cartão-para-cartão)¶
Exige status atual active ou pending e devolve o payload de cartão:
{
"subscription": {
"id": "507f1f77bcf86cd799439011",
"plan_id": "plan-basic",
"gateway": "asaas",
"status": "pending",
"payment_method": "credit_card",
"started_at": "2026-08-19T12:00:00+00:00"
},
"message": "Assinatura renovada com sucesso. Novo cartão configurado."
}
A nova assinatura nasce pending; o webhook do gateway (PAYMENT_RECEIVED no
Asaas, invoice.paid na Stripe) a promove para active. A tela deve dar o
feedback de "processando" e repolar GET /customers/me/subscription.
4.4 Erros do renew¶
| Status | Detalhe | Causa |
|---|---|---|
| 400 | Subscription is still active. A PIX renewal would revoke… |
Tentou renovar por PIX com assinatura ainda ativa |
| 400 | credit_card and credit_card_holder_info are required for Asaas gateway. |
Caminho cartão/Asaas sem dados do cartão |
| 400 | payment_method_id is required for Stripe gateway. |
Caminho cartão/Stripe sem token |
| 400 | Cannot renew subscription with status "cancelled". |
Caminho cartão só aceita active/pending — para reativar cancelada, use PIX ou assine de novo (§7) |
| 400 | Plan is no longer active |
Plano desativado — recarregue GET /plans |
| 400 | Cannot resolve a plan to renew into… |
Assinatura antiga sem plan_id e nenhum enviado |
| 404 | No active subscription found |
Sem current_subscription_id |
5. Retomar um PIX pendente (recuperar o QR)¶
Enquanto a assinatura está pending + pix, o QR pode ser rebuscado quantas
vezes for preciso — cada chamada gera uma nova janela de 10 minutos:
{
"subscription_id": "507f1f77bcf86cd799439011",
"status": "pending",
"gateway": "asaas",
"pix": {
"authorization_id": "pixauth_xxx",
"qr_code": "00020101...6304ABCD",
"qr_code_image": "iVBORw0KGgoAAAANS...",
"generated_at": "2026-08-19T12:00:00+00:00",
"expiration_date": "2026-08-19T12:10:00+00:00"
},
"message": "PIX QR code retrieved. Scan to complete payment."
}
- Asaas:
pix.qr_code(copia-e-cola) +pix.qr_code_image(base64 PNG, sem prefixodata:). - Stripe: os campos são
qr_code_image_url(URL PNG),expires_atehosted_instructions_url— trate os dois formatos.
| Status | Detalhe | Ação na UI |
|---|---|---|
| 400 | Subscription is not pending (current status: active) |
Já pago — atualize a tela |
| 400 | Subscription is not a PIX payment |
Assinatura de cartão |
| 404 | No PIX QR code available for this authorization |
Sem cobrança em aberto — recarregue o estado |
| 410 | PIX authorization is no longer payable… |
Autorização cancelada/expirada: cancele a assinatura e assine de novo |
Polling
Depois de exibir o QR, repolar GET /customers/me/subscription a cada ~5s.
A ativação vem por webhook; não existe endpoint de "confirmar pagamento".
6. Cancelar a assinatura¶
Use sempre a rota do customer — ela é a única que cobre todos os casos:
{
"message": "Subscription cancelled successfully",
"cancelled_at": "2026-08-19T12:00:00+00:00",
"gateway_response": { "...": "resposta bruta do gateway" },
"note": "Subscription cancelled in payment gateway and local database"
}
O que ela faz que as rotas por gateway não fazem por completo:
- Pix Automático: revoga a autorização e cancela a subscription wrapper que a Asaas cria junto. Revogar só a autorização interrompe o débito futuro mas deixa o wrapper emitindo uma cobrança PIX não paga por ciclo, para sempre.
- Falha honesta: se a chamada ao gateway falhar, o acesso é revogado
localmente e a resposta diz isso —
messagevira"Subscription cancelled locally; gateway cancellation failed and will be retried"e a assinatura fica marcada para reparo. Mostre esse texto ao cliente, não um "cancelado com sucesso" genérico.
Não é cancelamento no fim do período
DELETE /customers/me/subscription corta o acesso imediatamente (status
local vira cancelled e o ponteiro do cliente é limpo). Deixe isso explícito
na confirmação. (A rota específica DELETE /stripe/subscription cancela no
fim do período no lado da Stripe, mas marca cancelled local na hora — a
divergência é mais um motivo para usar a rota do customer.)
Um e-mail de cancelamento é enviado automaticamente pela API.
7. Assinar de novo depois de cancelar¶
Sem current_subscription_id, o caminho é o subscribe do gateway padrão
(GET /config/payment-gateway):
Guardas que a UI precisa respeitar
| Status | Detalhe | Significado |
|---|---|---|
| 400 | Customer already has an active subscription. Cancel the current subscription before subscribing again. |
Já existe assinatura ativa |
| 400 | Customer already has a pending payment… / pending Pix Automático authorization… |
Existe pendente com menos de 24h — leve o cliente ao QR (§5) em vez de criar outra. Pendente com mais de 24h é considerada abandonada e liberada automaticamente |
| 429 | Outra operação de assinatura já está em andamento. Aguarde alguns segundos. |
Lock anti-duplo-clique. Desabilite o botão durante a requisição e trate 429 como "aguarde", nunca como falha para retry imediato |
8. Fluxo de decisão da tela¶
GET /customers/me/subscription
│
┌─────────────────┼──────────────────┐
404 │ 200 │ status=pending │ 200 status=active
▼ ▼ ▼
┌────────────────┐ ┌──────────────┐ ┌──────────────────────┐
│ Sem assinatura │ │ payment_ │ │ payment_method? │
│ → §7 subscribe │ │ method=pix? │ └──────────┬───────────┘
└────────────────┘ └──────┬───────┘ │
│ sim ┌────────┴────────┐
▼ │ credit_card │ pix
┌────────────────────┐ ▼ ▼
│ §5 GET /me/pix-qr │ ┌──────────────┐ ┌────────────────┐
│ + polling │ │ §3 trocar │ │ Só cancelar + │
└────────────────────┘ │ cartão │ │ reassinar (§6, │
│ (in-place) │ │ §7). Renew PIX │
│ §4 trocar │ │ em ativa = 400 │
│ p/ PIX/plano │ └────────────────┘
└──────────────┘
status=expired / suspended / cancelled
│
▼
§4 renew (PIX) — permitido fora de "active"
ou §7 subscribe, se não há mais assinatura corrente
9. Exemplo Flutter (serviço enxuto)¶
class SubscriptionService {
SubscriptionService(this.baseUrl, this.token);
final String baseUrl;
final String token;
Map<String, String> get _headers => {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
};
/// Estado atual. Retorna null quando o cliente não tem assinatura (404).
Future<Map<String, dynamic>?> current() async {
final r = await http.get(
Uri.parse('$baseUrl/api/v1/customers/me/subscription'),
headers: _headers,
);
if (r.statusCode == 404) return null;
if (r.statusCode != 200) throw ApiException(r);
return jsonDecode(r.body)['subscription'] as Map<String, dynamic>;
}
/// Troca só o cartão, mantendo a recorrência. Escolhe a rota pelo gateway.
Future<void> updateCard(
String gateway, {
Map<String, dynamic>? creditCard,
Map<String, dynamic>? holderInfo,
String? paymentMethodId,
}) async {
final isAsaas = gateway == 'asaas';
final r = await http.put(
Uri.parse(isAsaas
? '$baseUrl/api/v1/asaas/subscription/credit-card'
: '$baseUrl/api/v1/stripe/subscription/payment-method'),
headers: _headers,
body: jsonEncode(isAsaas
? {'credit_card': creditCard, 'credit_card_holder_info': holderInfo}
: {'payment_method_id': paymentMethodId}),
);
if (r.statusCode != 200) throw ApiException(r);
}
/// Renova para PIX recorrente. Só chame com assinatura NÃO ativa (§4.2).
/// Retorna o bloco `pix` com o QR a renderizar.
Future<Map<String, dynamic>> renewWithPix({String? planId, String? promoCode}) async {
final r = await http.put(
Uri.parse('$baseUrl/api/v1/customers/me/subscription/renew'),
headers: _headers,
body: jsonEncode({
'payment_method': 'pix',
if (planId != null) 'plan_id': planId,
if (promoCode != null) 'promo_code': promoCode,
}),
);
if (r.statusCode != 200) throw ApiException(r);
return jsonDecode(r.body)['info']['pix'] as Map<String, dynamic>;
}
/// Recupera o QR de um PIX pendente (nova janela de 10 min a cada chamada).
Future<Map<String, dynamic>> pixQr() async {
final r = await http.get(
Uri.parse('$baseUrl/api/v1/customers/me/pix-qr'),
headers: _headers,
);
if (r.statusCode != 200) throw ApiException(r); // 410 => reassinar
return jsonDecode(r.body)['pix'] as Map<String, dynamic>;
}
/// Cancela. `gatewayOk == false` => avisar que a cobrança pode persistir.
Future<({String message, bool gatewayOk})> cancel() async {
final r = await http.delete(
Uri.parse('$baseUrl/api/v1/customers/me/subscription'),
headers: _headers,
);
if (r.statusCode != 200) throw ApiException(r);
final body = jsonDecode(r.body) as Map<String, dynamic>;
final message = body['message'] as String;
return (message: message, gatewayOk: !message.contains('gateway cancellation failed'));
}
}
Renderizar o QR do Asaas (base64 puro, sem prefixo data:):
10. Checklist de implementação¶
- [ ] Ler
gatewaydo GET antes de escolher rota de cartão (Asaas × Stripe). - [ ] Botão "trocar cartão" oculto em assinatura PIX (retorna 400).
- [ ] Botão "mudar para PIX" desabilitado enquanto
status == 'active'(400 por design). - [ ] Tratar toda resposta de
renewchecandoinfo.pixantes de assumir cartão. - [ ] Contagem regressiva de 10 min pelo
expiration_date/expires_atdo QR; botão "gerar novo QR" chamando/me/pix-qr. - [ ] Polling de
GET /customers/me/subscriptionapós pagar/renovar (ativação é por webhook). - [ ] 429 no subscribe = "aguarde", com botão desabilitado durante a chamada.
- [ ] Texto de cancelamento deixando claro que o acesso encerra na hora.
- [ ] Repassar a mensagem honesta de falha de cancelamento no gateway.
- [ ] Nunca chamar
POST /customers/me/subscribe(501).