Endpoints for the admin promo-code usage panel. Shows how promo codes translate into actual subscriptions, payments and discounts — with optional cross-verification against the Asaas gateway.
Prefix: /api/v1/admin/promo-usage
Authentication: Bearer token (JWT) + user_type='admin'
Endpoints
| Method |
Endpoint |
Description |
| GET |
/admin/promo-usage/dashboard |
KPIs + breakdown by code, gateway, payment method |
| GET |
/admin/promo-usage/subscriptions |
Paginated subscription list with optional gateway verification |
| GET |
/admin/promo-usage/export |
Download XLSX report |
Consolidated KPIs for promo code usage in subscriptions. Ready for dashboard cards and charts.
GET /api/v1/admin/promo-usage/dashboard
Authorization: Bearer <admin_token>
Query Params
| Param |
Type |
Default |
Description |
code |
string |
— |
Filter by specific promo code (e.g. ANGELA). Case-insensitive (uppercased internally). |
date_from |
datetime |
— |
Start date filter (UTC, ISO 8601) |
date_until |
datetime |
— |
End date filter (UTC, ISO 8601) |
Response (200 OK)
{
"overview": {
"total_uses": 101,
"total_paid": 93,
"total_pending": 7,
"total_overdue": 0,
"total_cancelled": 1,
"total_discount_given": 1454.40,
"total_revenue_collected": 555.50,
"total_revenue_full_price": 2009.90
},
"by_code": [
{
"code": "ANGELA",
"title": "Desconto no primeiro mês",
"is_active": true,
"discount_first_month": 5.50,
"total_uses": 101,
"paid": 93,
"pending": 7,
"overdue": 0,
"cancelled": 1,
"total_discount_given": 1454.40,
"total_revenue_collected": 555.50
}
],
"by_gateway": {
"asaas": {
"count": 101,
"total_discount_given": 1454.40
}
},
"by_payment_method": {
"pix": {
"count": 76,
"total_discount_given": 1094.40
},
"credit_card": {
"count": 25,
"total_discount_given": 360.00
}
}
}
Fields — overview
| Field |
Type |
Description |
total_uses |
int |
Total subscriptions with discount_applied=true |
total_paid |
int |
Subscriptions with status='active' |
total_pending |
int |
Subscriptions with status='pending' |
total_overdue |
int |
Subscriptions with status='suspended' |
total_cancelled |
int |
Subscriptions with status='cancelled' or 'expired' |
total_discount_given |
float |
Sum of discounts given in reais (full_price - collected) |
total_revenue_collected |
float |
Sum of discounted values charged (reais) |
total_revenue_full_price |
float |
What would have been charged without discount (reais) |
Fields — by_code (sorted by total_uses desc)
| Field |
Type |
Description |
code |
string |
Promo code string |
title |
string |
Promo code display title |
is_active |
bool |
Whether code is currently active |
discount_first_month |
float? |
Discounted price in reais (e.g. 5.50 = R$5,50) |
total_uses |
int |
Number of subscriptions that used this code |
paid / pending / overdue / cancelled |
int |
Status breakdown |
total_discount_given |
float |
Sum of discounts for this code (reais) |
total_revenue_collected |
float |
Sum of discounted values for this code (reais) |
Fields — by_gateway / by_payment_method
| Field |
Type |
Description |
count |
int |
Number of subscriptions |
total_discount_given |
float |
Sum of discounts (reais) |
Flutter/Dart
class PromoUsageOverview {
final int totalUses;
final int totalPaid;
final int totalPending;
final int totalOverdue;
final int totalCancelled;
final double totalDiscountGiven;
final double totalRevenueCollected;
final double totalRevenueFullPrice;
PromoUsageOverview.fromJson(Map<String, dynamic> json)
: totalUses = json['total_uses'],
totalPaid = json['total_paid'],
totalPending = json['total_pending'],
totalOverdue = json['total_overdue'],
totalCancelled = json['total_cancelled'],
totalDiscountGiven = (json['total_discount_given'] as num).toDouble(),
totalRevenueCollected = (json['total_revenue_collected'] as num).toDouble(),
totalRevenueFullPrice = (json['total_revenue_full_price'] as num).toDouble();
}
class PromoUsageByCode {
final String code;
final String title;
final bool isActive;
final double? discountFirstMonth;
final int totalUses;
final int paid;
final int pending;
final int overdue;
final int cancelled;
final double totalDiscountGiven;
final double totalRevenueCollected;
PromoUsageByCode.fromJson(Map<String, dynamic> json)
: code = json['code'],
title = json['title'],
isActive = json['is_active'],
discountFirstMonth = (json['discount_first_month'] as num?)?.toDouble(),
totalUses = json['total_uses'],
paid = json['paid'],
pending = json['pending'],
overdue = json['overdue'],
cancelled = json['cancelled'],
totalDiscountGiven = (json['total_discount_given'] as num).toDouble(),
totalRevenueCollected = (json['total_revenue_collected'] as num).toDouble();
}
class PromoUsageDashboard {
final PromoUsageOverview overview;
final List<PromoUsageByCode> byCode;
final Map<String, Map<String, dynamic>> byGateway;
final Map<String, Map<String, dynamic>> byPaymentMethod;
PromoUsageDashboard.fromJson(Map<String, dynamic> json)
: overview = PromoUsageOverview.fromJson(json['overview']),
byCode = (json['by_code'] as List)
.map((e) => PromoUsageByCode.fromJson(e))
.toList(),
byGateway = Map<String, Map<String, dynamic>>.from(
(json['by_gateway'] as Map).map(
(k, v) => MapEntry(k as String, Map<String, dynamic>.from(v)))),
byPaymentMethod = Map<String, Map<String, dynamic>>.from(
(json['by_payment_method'] as Map).map(
(k, v) => MapEntry(k as String, Map<String, dynamic>.from(v))));
}
Future<PromoUsageDashboard> fetchPromoUsageDashboard({
required String token,
String? code,
DateTime? dateFrom,
DateTime? dateUntil,
}) async {
final response = await dio.get(
'/api/v1/admin/promo-usage/dashboard',
queryParameters: {
if (code != null) 'code': code,
if (dateFrom != null) 'date_from': dateFrom.toIso8601String(),
if (dateUntil != null) 'date_until': dateUntil.toIso8601String(),
},
options: Options(headers: {'Authorization': 'Bearer $token'}),
);
return PromoUsageDashboard.fromJson(response.data);
}
Paginated list of individual subscriptions that used a promo code. With verify_gateway=true, each subscription includes real-time data from the Asaas API (actual value charged, payment status, payment date).
GET /api/v1/admin/promo-usage/subscriptions?code=ANGELA&verify_gateway=true
Authorization: Bearer <admin_token>
Query Params
| Param |
Type |
Default |
Description |
code |
string |
— |
Filter by promo code (case-insensitive) |
status |
string |
— |
Filter by subscription status (active, pending, suspended, cancelled, expired) |
gateway |
string |
— |
Filter by gateway (asaas, stripe) |
date_from |
datetime |
— |
Start date filter (UTC) |
date_until |
datetime |
— |
End date filter (UTC) |
verify_gateway |
bool |
false |
Cross-check with Asaas API for actual payment data |
page |
int |
0 |
Page offset (0-based) |
limit |
int |
50 |
Items per page (max 500) |
Performance: verify_gateway
When verify_gateway=true, the API makes one HTTP request to Asaas per subscription. For large result sets, use pagination (limit=20-50) to keep response times reasonable. Without this flag, data comes only from MongoDB (fast).
Response (200 OK)
{
"total": 101,
"subscriptions": [
{
"subscription_id": "69d851b2ade1886417f3e9bb",
"customer_id": "69d6f7cab207249b98f3e9c7",
"customer_name": "Aline Machado Soares dos Santos",
"username": "alineprof9331@gmail.com",
"promo_code_used": "ANGELA",
"gateway": "asaas",
"payment_method": "pix",
"status": "active",
"discount_first_month": 5.50,
"original_amount_reais": 19.90,
"discount_given_reais": 14.40,
"created_at": "2026-04-10T01:26:09",
"gateway_payment": {
"gateway_subscription_id": "sub_gs0j1r7kvjj2sgp9",
"gateway_payment_id": "pay_g2t8j9vi7jdqpwde",
"gateway_value": 5.50,
"gateway_status": "RECEIVED",
"gateway_billing_type": "PIX",
"gateway_due_date": "2026-04-10",
"gateway_payment_date": "2026-04-09",
"gateway_error": null
}
}
]
}
Fields — subscription item
| Field |
Type |
Description |
subscription_id |
string |
MongoDB ObjectId |
customer_id |
string |
Customer ObjectId |
customer_name |
string |
Customer full name (from users collection via $lookup) |
username |
string |
Customer username/email |
promo_code_used |
string |
Promo code string used |
gateway |
string |
Payment gateway (asaas or stripe) |
payment_method |
string |
pix or credit_card |
status |
string |
Internal subscription status (active, pending, suspended, cancelled, expired) |
discount_first_month |
float? |
Discounted price in reais (e.g. 5.50) |
original_amount_reais |
float? |
Original plan price in reais (e.g. 19.90) |
discount_given_reais |
float |
Discount amount (original - discounted) |
created_at |
string? |
ISO 8601 datetime |
gateway_payment |
object? |
null when verify_gateway=false, populated when true |
Fields — gateway_payment (when verify_gateway=true)
| Field |
Type |
Description |
gateway_subscription_id |
string? |
Asaas subscription ID (sub_xxx) |
gateway_payment_id |
string? |
Asaas payment ID (pay_xxx) |
gateway_value |
float? |
Actual value charged by the gateway in reais |
gateway_status |
string? |
Gateway payment status: RECEIVED, CONFIRMED, PENDING, OVERDUE |
gateway_billing_type |
string? |
PIX or CREDIT_CARD |
gateway_due_date |
string? |
Payment due date (YYYY-MM-DD) |
gateway_payment_date |
string? |
Date customer actually paid (YYYY-MM-DD, null if not yet paid) |
gateway_error |
string? |
Error message if gateway query failed |
Credit card vs PIX verification
- PIX: Fetches the first (oldest) payment from the Asaas subscription.
- Credit card with discount: Fetches the standalone first-month payment (stored as
first_month_payment_id in MongoDB), since the Asaas subscription is created at full price for future months.
Flutter/Dart
class GatewayPaymentInfo {
final String? gatewaySubscriptionId;
final String? gatewayPaymentId;
final double? gatewayValue;
final String? gatewayStatus;
final String? gatewayBillingType;
final String? gatewayDueDate;
final String? gatewayPaymentDate;
final String? gatewayError;
GatewayPaymentInfo.fromJson(Map<String, dynamic> json)
: gatewaySubscriptionId = json['gateway_subscription_id'],
gatewayPaymentId = json['gateway_payment_id'],
gatewayValue = (json['gateway_value'] as num?)?.toDouble(),
gatewayStatus = json['gateway_status'],
gatewayBillingType = json['gateway_billing_type'],
gatewayDueDate = json['gateway_due_date'],
gatewayPaymentDate = json['gateway_payment_date'],
gatewayError = json['gateway_error'];
/// True if the gateway confirmed the payment was received
bool get isPaid =>
gatewayStatus == 'RECEIVED' || gatewayStatus == 'CONFIRMED';
/// True if the discount was correctly applied at gateway level
bool get discountVerified => gatewayValue != null && gatewayError == null;
}
class PromoUsageSubscription {
final String subscriptionId;
final String customerId;
final String customerName;
final String username;
final String promoCodeUsed;
final String gateway;
final String paymentMethod;
final String status;
final double? discountFirstMonth;
final double? originalAmountReais;
final double discountGivenReais;
final String? createdAt;
final GatewayPaymentInfo? gatewayPayment;
PromoUsageSubscription.fromJson(Map<String, dynamic> json)
: subscriptionId = json['subscription_id'],
customerId = json['customer_id'],
customerName = json['customer_name'],
username = json['username'],
promoCodeUsed = json['promo_code_used'],
gateway = json['gateway'],
paymentMethod = json['payment_method'],
status = json['status'],
discountFirstMonth = (json['discount_first_month'] as num?)?.toDouble(),
originalAmountReais = (json['original_amount_reais'] as num?)?.toDouble(),
discountGivenReais = (json['discount_given_reais'] as num).toDouble(),
createdAt = json['created_at'],
gatewayPayment = json['gateway_payment'] != null
? GatewayPaymentInfo.fromJson(json['gateway_payment'])
: null;
}
Future<Map<String, dynamic>> fetchPromoUsageSubscriptions({
required String token,
String? code,
String? status,
String? gateway,
bool verifyGateway = false,
int page = 0,
int limit = 50,
}) async {
final response = await dio.get(
'/api/v1/admin/promo-usage/subscriptions',
queryParameters: {
if (code != null) 'code': code,
if (status != null) 'status': status,
if (gateway != null) 'gateway': gateway,
'verify_gateway': verifyGateway,
'page': page,
'limit': limit,
},
options: Options(headers: {'Authorization': 'Bearer $token'}),
);
return response.data; // { total, subscriptions: [...] }
}
Download promo usage report in xlsx (default), csv or json. By default includes gateway verification columns. Style follows ADR-059.
GET /api/v1/admin/promo-usage/export?code=ANGELA&format=xlsx
Authorization: Bearer <admin_token>
Query Params
| Param |
Type |
Default |
Description |
format |
string |
xlsx |
xlsx / csv / json |
code |
string |
— |
Filter by promo code |
date_from |
datetime |
— |
Start date filter (UTC) |
date_until |
datetime |
— |
End date filter (UTC) |
verify_gateway |
bool |
true |
Include gateway verification columns (default ON for exports) |
token |
string |
— |
Bearer token (alternative to Authorization header — for direct URL downloads) |
Response
format |
Content-Type |
Filename |
xlsx |
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet |
promo_usage_<CODE>_<TS>.xlsx |
csv |
text/csv; charset=utf-8 |
promo_usage_<CODE>_<TS>.csv |
json |
application/json; charset=utf-8 |
promo_usage_<CODE>_<TS>.json |
Sheet — "Uso de Cupons"
Base columns (always present):
| Column |
Description |
| ID |
Subscription ObjectId |
| Cliente |
Customer full name |
| Username |
Customer username/email |
| Email |
Customer email |
| Cupom |
Promo code used |
| Gateway |
asaas or stripe |
| Pagamento |
pix or credit_card |
| Status (interno) |
Internal subscription status |
| Valor Original (R$) |
Plan price before discount (e.g. 19.90) |
| Valor Cobrado (R$) |
Discounted price charged (e.g. 5.50) |
| Desconto (R$) |
Discount amount (e.g. 14.40) |
| Criado em |
DD/MM/YYYY HH:MM |
Gateway columns (when verify_gateway=true):
| Column |
Description |
| Asaas Sub ID |
Asaas subscription ID (sub_xxx) |
| Asaas Payment ID |
Asaas payment ID (pay_xxx) |
| Valor Gateway (R$) |
Actual value charged by Asaas |
| Status Gateway |
RECEIVED, CONFIRMED, PENDING, OVERDUE |
| Tipo Cobranca |
PIX or CREDIT_CARD |
| Vencimento |
Payment due date |
| Data Pagamento |
Date customer paid (empty if not yet paid) |
| Erro Gateway |
Error if Asaas query failed |
Flutter/Dart — Download and save
Modo 1 — Dio com header (recomendado):
Future<File> downloadPromoUsageXlsx({
required String token,
String? code,
bool verifyGateway = true,
}) async {
final response = await Dio().get(
'$baseUrl/api/v1/admin/promo-usage/export',
queryParameters: {
if (code != null) 'code': code,
'verify_gateway': verifyGateway,
},
options: Options(
headers: {'Authorization': 'Bearer $token'},
responseType: ResponseType.bytes,
),
);
final dir = await getApplicationDocumentsDirectory();
final filename = 'promo_usage_${DateTime.now().millisecondsSinceEpoch}.xlsx';
final file = File('${dir.path}/$filename');
await file.writeAsBytes(response.data as List<int>);
return file;
}
Modo 2 — URL direta com token como query param (para launchUrl, WebView, share link):
Future<void> openPromoUsageXlsx(String token, {String? code}) async {
final params = <String, String>{'token': token};
if (code != null) params['code'] = code;
final uri = Uri.parse('$baseUrl/api/v1/admin/promo-usage/export')
.replace(queryParameters: params);
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
UI Suggestions
Dashboard Cards (from /dashboard)
| Card |
Value |
Color Logic |
| Total de Usos |
overview.total_uses |
— |
| Pagos |
overview.total_paid |
Green |
| Pendentes |
overview.total_pending |
Yellow |
| Cancelados |
overview.total_cancelled |
Red |
| Desconto Concedido |
R$ overview.total_discount_given |
— |
| Receita Coletada |
R$ overview.total_revenue_collected |
— |
Filters Bar
- Dropdown "Cupom": Populate from
by_code[].code (or a separate promo codes list endpoint)
- Date range picker: Maps to
date_from / date_until
- Toggle "Verificar Gateway": Controls
verify_gateway param on the table
Data Table (from /subscriptions)
| Column |
Source |
Notes |
| Cliente |
customer_name |
Primary column |
| Email |
username |
— |
| Cupom |
promo_code_used |
— |
| Metodo |
payment_method |
Badge: PIX / Cartao |
| Valor Original |
original_amount_reais |
Format as R$ 19,90 |
| Valor Cobrado |
discount_first_month |
Format as R$ 5,50 |
| Desconto |
discount_given_reais |
Format as R$ 14,40 |
| Status |
status |
Badge with color |
| Valor Gateway |
gateway_payment.gateway_value |
Only when verify enabled |
| Status Gateway |
gateway_payment.gateway_status |
Badge: RECEIVED=green, PENDING=yellow, OVERDUE=red |
| Data Pagamento |
gateway_payment.gateway_payment_date |
Only when verify enabled |
[Download XLSX] → calls /export?code={selected_code}&verify_gateway=true
Notes
- Revenue in reais (not cents). e.g.
555.50 = R$555,50.
- Admin authentication required on all endpoints.
- Status mapping:
active = paid, pending = awaiting payment, suspended = overdue, cancelled/expired = cancelled.
- Gateway status values: Asaas returns
RECEIVED (PIX paid), CONFIRMED (credit card confirmed), PENDING (awaiting), OVERDUE (past due).
- Credit card discount flow: First month is a standalone Asaas payment (not part of the subscription). The subscription starts at full price for month 2+. The
gateway_payment field correctly fetches the standalone payment when first_month_payment_id exists.
- Aggregation source:
/dashboard uses MongoDB aggregation only (fast). /subscriptions?verify_gateway=true makes additional HTTP calls to Asaas (slower, use pagination).