Skip to content

Reembolso — Guia de Integracao Frontend (Admin)

Endpoint

POST /api/v1/subscriptions/{subscription_id}/refund
Authorization: Bearer <admin_token>
Content-Type: application/json

Payload

{
  "reason": "Motivo do reembolso (min 5 chars, obrigatorio)"
}

Apenas reason e obrigatorio. O frontend NAO precisa enviar IDs de pagamento — o backend auto-detecta o gateway e busca o ultimo pagamento automaticamente.

Como funciona internamente:

  • Stripe: backend le stripe_customer_id da subscription → lista charges no Stripe → reembolsa o ultimo
  • Asaas: backend le asaas_subscription_id → lista pagamentos no Asaas → reembolsa o ultimo

O frontend so envia o motivo. Nao precisa saber qual gateway e nem buscar IDs.

Campos opcionais (uso interno/debug)

payment_intent_id, payment_id e charge_id existem no schema mas sao para uso interno. O unico cenario onde o frontend precisaria enviar payment_intent_id e se o customer foi deletado antes do reembolso (o backend perde o stripe_customer_id). Na pratica, sempre reembolse antes de deletar o customer.


Deteccao automatica por gateway

O backend le o campo gateway da subscription e despacha:

Gateway Como o backend reembolsa Campo opcional
Stripe (cartao) Busca ultimo charge via stripe_customer_idrefund payment_intent_id (se customer deletado)
Asaas (cartao/PIX) Busca ultimo pagamento via asaas_subscription_idrefund payment_id (se quiser reembolsar pagamento especifico)

Response (200 OK)

{
  "docs": [{ "...subscription atualizada..." }],
  "info": {
    "message": "Refund processed successfully",
    "subscription_id": "69ca960e...",
    "gateway": "stripe",
    "refund_type": "stripe_refund",
    "refund_amount": 1990,
    "gateway_response": {
      "id": "re_3TGhhU...",
      "amount": 1990,
      "currency": "brl",
      "status": "succeeded",
      "charge": "ch_3TGhhU...",
      "payment_intent": "pi_3TGhhU..."
    },
    "reason": "Motivo informado pelo admin",
    "refunded_at": "2026-03-30T15:31:49Z",
    "admin": "root"
  }
}

Errors

HTTP Codigo Cenario
404 NOT_FOUND Subscription nao encontrada
422 REFUND_NOT_ALLOWED Status nao permite reembolso
422 VALIDATION_ERROR Customer sem stripe_customer_id (passar payment_intent_id manualmente)
500 REFUND_FAILED Falha no gateway (Stripe/Asaas rejeitou)

Consultar historico de reembolsos

GET /api/v1/subscriptions/{subscription_id}/refunds
Authorization: Bearer <admin_token>

Retorna lista de reembolsos e chargebacks da subscription.


Fluxo recomendado na UI admin

1. Admin abre detalhes da subscription
2. Clica "Reembolsar"
3. Modal pede "Motivo" (textarea, min 5 chars)
4. POST /subscriptions/{id}/refund com {"reason": "..."}
5. Exibir resultado:
   - Sucesso: "Reembolso de R$ X,XX processado (ID: re_xxx)"
   - Erro: exibir message do error response
6. Atualizar dados da subscription (total_refunded mudou)

Dart/Flutter

/// Solicitar reembolso de uma subscription (admin only).
///
/// [subscriptionId] — ID da subscription no MongoDB
/// [reason] — Motivo do reembolso (min 5 chars)
/// [paymentIntentId] — Opcional, necessario se customer foi deletado (Stripe)
Future<Map<String, dynamic>> refundSubscription({
  required String token,
  required String subscriptionId,
  required String reason,
  String? paymentIntentId,
}) async {
  final body = <String, dynamic>{'reason': reason};
  if (paymentIntentId != null) body['payment_intent_id'] = paymentIntentId;

  final resp = await http.post(
    Uri.parse('$baseUrl/api/v1/subscriptions/$subscriptionId/refund'),
    headers: {
      'Authorization': 'Bearer $token',
      'Content-Type': 'application/json',
    },
    body: jsonEncode(body),
  );

  final data = jsonDecode(resp.body);
  if (resp.statusCode == 200) return data;
  throw Exception(data['error']?['message'] ?? 'Erro ao reembolsar');
}

Exibir valor reembolsado

/// Converter centavos para reais formatado.
String formatCents(int cents) => 'R\$ ${(cents / 100).toStringAsFixed(2)}';

// Exemplo: formatCents(1990) → "R$ 19,90"

Rotas de Reembolso por Gateway (Admin)

Alem da rota generica /subscriptions/{id}/refund, existem rotas gateway-specific que validam o gateway antes de operar:

Stripe

Metodo Endpoint Descricao
POST /api/v1/admin/stripe/subscriptions/{id}/refund Estornar pagamento Stripe
GET /api/v1/admin/stripe/subscriptions/{id}/refunds Historico de estornos Stripe

Request Body (Stripe):

{
  "payment_intent_id": "pi_xxx",
  "reason": "Customer requested refund"
}
Campo Tipo Obrigatorio Descricao
payment_intent_id string Nao Stripe PaymentIntent ID (pi_xxx). Auto-detecta se omitido
reason string Sim Motivo do reembolso (5-500 chars)

O event_type no audit log e ADMIN.REFUND.STRIPE_REFUND.

Asaas

Metodo Endpoint Descricao
POST /api/v1/admin/asaas/subscriptions/{id}/refund Estornar pagamento Asaas
GET /api/v1/admin/asaas/subscriptions/{id}/refunds Historico de estornos Asaas

Request Body (Asaas):

{
  "payment_id": "pay_xxx",
  "value": 49.90,
  "reason": "Customer requested refund"
}
Campo Tipo Obrigatorio Descricao
payment_id string Nao Asaas Payment ID (pay_xxx). Auto-detecta se omitido
value float Nao Valor a estornar em reais (None = reembolso total)
reason string Sim Motivo do reembolso (5-500 chars)

O event_type no audit log e ADMIN.REFUND.ASAAS_REFUND.

Recomendacao

Prefira usar as rotas gateway-specific (/admin/stripe/... ou /admin/asaas/...) ao inves da rota generica. Elas validam o gateway antes de operar e evitam operacoes acidentais no gateway errado.

Para exemplos completos de request/response, consulte API de Administracao e API de Administracao — Asaas.


Notas

  1. Valores em centavosrefund_amount: 1990 = R$19,90. Frontend converte / 100
  2. total_refunded na subscription acumula todos os reembolsos (campo em centavos)
  3. Reembolso Stripe demora 5-10 dias uteis para aparecer no cartao do cliente
  4. Reembolso Asaas PIX e processado em ate 24h pela Asaas
  5. Audit trail — todas as acoes de reembolso sao registradas em webhook_logs com event_type prefixado ADMIN.REFUND.STRIPE_REFUND ou ADMIN.REFUND.ASAAS_REFUND