Customers API — Cadastro e Gerenciamento¶
Base Path: /api/v1/customers
📘 Guia para Desenvolvedores Frontend
Novo no projeto? Veja o Guia do Frontend com exemplos completos em Flutter/Dart para implementar cadastro e assinatura de customers.
📋 Endpoints¶
| Método | Endpoint | Descrição | Auth | Subscription |
|---|---|---|---|---|
| GET | /customers/check-availability |
Verificar disponibilidade de email/CPF | ❌ Pública | - |
| POST | /customers/signup |
Cadastro com verificacao de email (freemium, 2 passos) | ❌ Publica | - |
| POST | /customers/me/subscribe |
Assinatura posterior (customer autenticado) | ✅ JWT | - |
| GET | /customers/me |
Buscar dados do customer autenticado | ✅ JWT | - |
| PUT | /customers/me |
Atualizar dados do customer (email bloqueado) | ✅ JWT | - |
| POST | /customers/me/change-email |
Solicitar alteracao de email (envia codigo) | ✅ JWT | - |
| POST | /customers/me/confirm-email |
Confirmar alteracao de email (valida codigo) | ✅ JWT | - |
| GET | /customers/me/subscription |
Buscar assinatura ativa | ✅ JWT | - |
| GET | /customers/validate-promo-code |
Validar codigo promo (auth opcional) | ❌ Publica | - |
| PUT | /customers/me/redeem-promo-code |
Resgatar codigo promo (consome para o customer) | ✅ JWT | - |
| GET | /customers/me/promo-codes |
Listar codigos promo com detalhes e flag redeemed | ✅ JWT | - |
| GET | /customers/me/pix-qr |
Recuperar QR code PIX (subscription pending) | ✅ JWT | - |
| PUT | /customers/me/subscription/renew |
Trocar metodo de pagamento | ✅ JWT | - |
| DELETE | /customers/me/subscription |
Cancelar assinatura | ✅ JWT | - |
| DELETE | /customers/me |
Deletar conta (self-service, cascade) | ✅ JWT | - |
| GET | /customers |
Listar customers (admin) | ✅ Admin | - |
| PUT | /customers |
Atualizar customer (admin) | ✅ Admin | - |
| DELETE | /customers |
Deletar customer (admin) | ✅ Admin | - |
🔑 Alterar Senha do Customer¶
Use o endpoint dedicado POST /api/v1/auth/change-password para alterar a senha.
Este endpoint funciona para qualquer usuário autenticado (Customer ou Admin) e não requer o hash atual da senha.
Endpoint Recomendado
Use POST /api/v1/auth/change-password ao invés de PUT /customers/me para alteração de senha.
Vantagens:
- Não requer o hash da senha (que não é retornado pela API)
- Funciona para Customers e Admins
- Limpa automaticamente a flag
password_reset_requiredpara usuários migrados
Fluxo de Alteração de Senha¶
sequenceDiagram
participant Frontend
participant API
participant MongoDB
Frontend->>API: POST /auth/change-password
Note over Frontend: Authorization: Bearer <token>
API->>MongoDB: Buscar usuário pelo token
API->>API: Verificar senha atual (Argon2)
alt Senha atual correta
API->>API: Hash nova senha (Argon2)
API->>MongoDB: Atualizar password hash
API-->>Frontend: 200 OK
else Senha atual incorreta
API-->>Frontend: 400 Bad Request
end
Request¶
POST /api/v1/auth/change-password
Authorization: Bearer eyJhbGci...
Content-Type: application/json
{
"current_password": "senha_atual_123",
"new_password": "nova_senha_segura_456"
}
Campos¶
| Campo | Tipo | Obrigatório | Validação | Descrição |
|---|---|---|---|---|
current_password |
string |
✅ Sim | mínimo 1 char | Senha atual |
new_password |
string |
✅ Sim | mínimo 8 chars | Nova senha |
Response (200 OK)¶
Errors¶
400 - Senha Atual Incorreta¶
404 - Usuário Não Encontrado¶
Exemplo Flutter/Dart¶
import 'dart:convert';
import 'package:http/http.dart' as http;
class AuthService {
final String baseUrl;
final String token;
AuthService({required this.baseUrl, required this.token});
/// Altera a senha do usuário autenticado (Customer ou Admin)
///
/// [currentPassword] - Senha atual do usuário
/// [newPassword] - Nova senha (mínimo 8 caracteres)
Future<bool> changePassword({
required String currentPassword,
required String newPassword,
}) async {
final response = await http.post(
Uri.parse('$baseUrl/api/v1/auth/change-password'),
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
},
body: jsonEncode({
'current_password': currentPassword,
'new_password': newPassword,
}),
);
if (response.statusCode == 200) {
return true;
} else {
final error = jsonDecode(response.body);
throw Exception(error['detail'] ?? 'Failed to change password');
}
}
}
// Uso:
final authService = AuthService(
baseUrl: 'https://fdplay-api.infraifd.com',
token: token,
);
await authService.changePassword(
currentPassword: 'senha_atual_123',
newPassword: 'nova_senha_segura_456',
);
Documentação Completa
Para mais detalhes sobre autenticação e fluxo de reset de senha obrigatório (usuários migrados), consulte API de Autenticação.
✏️ PUT /customers/me — Alterar Dados Pessoais¶
Atualizar dados pessoais do customer autenticado.
Todos os campos são opcionais. Envie apenas os campos que deseja alterar.
Request¶
PUT /api/v1/customers/me
Authorization: Bearer eyJhbGci...
Content-Type: application/json
[{
"full_name": "João Pedro da Silva",
"phone": "+5511999887766",
"address": {
"street": "Av. Paulista",
"number": "1000",
"complement": "Apto 123",
"locality": "Bela Vista",
"city": "São Paulo",
"region_code": "SP",
"country": "BRA",
"postal_code": "01310100"
}
}]
Email nao pode ser alterado via PUT
O campo email esta bloqueado neste endpoint. Enviar email no payload retorna 400 Bad Request.
Para alterar o email, use o fluxo de re-verificacao:
POST /customers/me/change-email → POST /customers/me/confirm-email
Campos Editáveis¶
| Campo | Tipo | Validação | Descrição |
|---|---|---|---|
username |
string |
3-50 chars, único | Nome de usuário |
full_name |
string |
3-200 chars | Nome completo |
tax_id |
string |
CPF (11) ou CNPJ (14) | CPF/CNPJ |
phone |
string |
Formato internacional | Telefone |
address |
object |
Ver estrutura abaixo | Endereço |
avatar_id |
ObjectId |
ID válido do GridFS | Avatar |
my_list |
list[string] |
Lista de strings | Lista pessoal do customer |
Estrutura do Endereço¶
| Campo | Tipo | Obrigatório | Validação | Descrição |
|---|---|---|---|---|
street |
string |
✅ Sim | 3-200 chars | Logradouro |
number |
string |
✅ Sim | 1-20 chars | Número |
complement |
string |
❌ Não | max 100 chars | Complemento (Apto, Sala, Bloco) |
locality |
string |
✅ Sim | 3-100 chars | Bairro |
city |
string |
✅ Sim | 3-100 chars | Cidade |
region_code |
string |
✅ Sim | 2 chars | UF (ex: SP, RJ) |
country |
string |
✅ Sim | 3 chars | País (BRA) |
postal_code |
string |
✅ Sim | 8 dígitos | CEP sem traço |
Campo complement é opcional
O campo complement é opcional no backend (default: string vazia, max 100 chars).
Response (200 OK)¶
{
"docs": [{
"_id": "507f1f77bcf86cd799439011",
"username": "joao_silva",
"email": "joao@example.com",
"full_name": "João Pedro da Silva",
"phone": "+5511999887766",
"address": {
"street": "Av. Paulista",
"number": "1000",
"complement": "Apto 123",
"locality": "Bela Vista",
"city": "São Paulo",
"region_code": "SP",
"country": "BRA",
"postal_code": "01310100"
},
"my_list": [],
"updated_at": "2026-01-26T15:45:00Z"
}],
"info": {},
"current_page": 0,
"qty_docs_page": 1,
"qty_docs_total": 1,
"next_page": null,
"previous_page": null,
"pages": 1
}
Errors¶
409 - Username Já Existe¶
400 - Email Bloqueado¶
Exemplo Flutter/Dart¶
import 'dart:convert';
import 'package:http/http.dart' as http;
class CustomerService {
final String baseUrl;
final String token;
CustomerService({required this.baseUrl, required this.token});
/// Atualiza dados pessoais do customer
Future<Map<String, dynamic>> updateProfile({
String? fullName,
String? phone,
Map<String, String>? address,
List<String>? myList,
}) async {
final Map<String, dynamic> payload = {};
if (fullName != null) payload['full_name'] = fullName;
if (phone != null) payload['phone'] = phone;
if (address != null) payload['address'] = address;
if (myList != null) payload['my_list'] = myList;
final response = await http.put(
Uri.parse('$baseUrl/api/v1/customers/me'),
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
},
body: jsonEncode([payload]),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return data['docs'][0];
} else {
final error = jsonDecode(response.body);
throw Exception(error['error']['message'] ?? 'Failed to update profile');
}
}
}
// Uso:
final service = CustomerService(baseUrl: 'https://api.fdplay.com', token: token);
await service.updateProfile(
fullName: 'João Pedro da Silva',
phone: '+5511999887766',
address: {
'street': 'Av. Paulista',
'number': '1000',
'complement': 'Apto 123',
'locality': 'Bela Vista',
'city': 'São Paulo',
'region_code': 'SP',
'country': 'BRA',
'postal_code': '01310100',
},
);
📧 Alterar Email do Customer¶
Fluxo de 2 etapas com re-verificacao por codigo de 6 digitos.
O email antigo permanece ativo ate que o novo seja confirmado.
Fluxo de Alteracao de Email¶
sequenceDiagram
participant Frontend
participant API
participant Email
Frontend->>API: POST /customers/me/change-email
Note over Frontend: Authorization: Bearer <token>
API->>API: Validar novo email (unico, diferente do atual)
API->>Email: Enviar codigo 6 digitos ao NOVO email
API-->>Frontend: 200 {message, new_email, expires_in_minutes}
Note over Frontend: Usuario recebe codigo no novo email
Frontend->>API: POST /customers/me/confirm-email
Note over Frontend: Authorization: Bearer <token>
API->>API: Validar codigo (SHA-256, max 5 tentativas)
API->>API: Atualizar email + username
API-->>Frontend: 200 {docs: [customer atualizado]}
Step 1: POST /customers/me/change-email¶
Solicitar alteracao de email. Envia codigo de verificacao ao novo email.
Request¶
POST /api/v1/customers/me/change-email
Authorization: Bearer eyJhbGci...
Content-Type: application/json
{
"new_email": "novo@example.com"
}
Response (200 OK)¶
{
"message": "Codigo de verificacao enviado para o novo email.",
"new_email": "novo@example.com",
"expires_in_minutes": 15
}
Errors¶
| Status | Detalhe | Causa |
|---|---|---|
| 400 | O novo email deve ser diferente do atual. |
Email igual ao atual |
| 409 | Este email ja esta em uso. |
Email pertence a outro usuario |
| 429 | Aguarde X segundos para reenviar o codigo. |
Rate limit (1 por minuto) |
Step 2: POST /customers/me/confirm-email¶
Confirmar alteracao com o codigo de 6 digitos recebido no novo email.
Request¶
POST /api/v1/customers/me/confirm-email
Authorization: Bearer eyJhbGci...
Content-Type: application/json
{
"verification_code": "092250"
}
Response (200 OK)¶
{
"docs": [{
"_id": "507f1f77bcf86cd799439011",
"username": "novo@example.com",
"email": "novo@example.com",
"full_name": "Joao da Silva",
"email_verified": true,
"email_verified_at": "2026-02-10T18:30:00Z",
"updated_at": "2026-02-10T18:30:00Z"
}],
"info": {
"message": "Email alterado com sucesso."
},
"current_page": 0,
"qty_docs_page": 1,
"qty_docs_total": 1,
"next_page": null,
"previous_page": null,
"pages": 1
}
Errors¶
| Status | Detalhe | Causa |
|---|---|---|
| 400 | Nenhuma alteracao de email pendente. |
Nao foi solicitado change-email antes |
| 400 | Codigo expirado. Solicite um novo codigo. |
Codigo expirou (15 minutos) |
| 400 | Codigo incorreto. X tentativa(s) restante(s). |
Codigo errado |
| 409 | Este email ja esta em uso. |
Race condition — outro usuario usou o email |
| 429 | Maximo de tentativas atingido. Solicite um novo codigo. |
5 tentativas erradas |
Exemplo Flutter/Dart¶
/// Solicitar alteracao de email (Step 1)
Future<Map<String, dynamic>> requestEmailChange(String newEmail) async {
final response = await http.post(
Uri.parse('$baseUrl/api/v1/customers/me/change-email'),
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
},
body: jsonEncode({'new_email': newEmail}),
);
if (response.statusCode == 200) {
return jsonDecode(response.body);
}
throw Exception(jsonDecode(response.body)['detail']);
}
/// Confirmar alteracao de email (Step 2)
Future<Map<String, dynamic>> confirmEmailChange(String code) async {
final response = await http.post(
Uri.parse('$baseUrl/api/v1/customers/me/confirm-email'),
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
},
body: jsonEncode({'verification_code': code}),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return data['docs'][0];
}
throw Exception(jsonDecode(response.body)['detail']);
}
🔍 GET /customers/check-availability¶
Verificar se email ou CPF/CNPJ está disponível para cadastro.
Endpoint público (sem autenticação). Use antes de submeter o formulário de cadastro para validação em tempo real.
Request¶
Parâmetros (Query String)¶
| Parâmetro | Tipo | Obrigatório | Descrição |
|---|---|---|---|
email |
string |
Não* | Email a verificar |
tax_id |
string |
Não* | CPF (11 dígitos) ou CNPJ (14 dígitos) |
*Pelo menos um parâmetro é obrigatório.
Response (200 OK)¶
| Campo | Tipo | Descrição |
|---|---|---|
email_available |
bool \| null |
true = disponível, false = já cadastrado, null = não verificado |
tax_id_available |
bool \| null |
true = disponível, false = já cadastrado, null = não verificado |
Errors¶
400 - Missing Parameters¶
Exemplo cURL¶
# Verificar email
curl "http://localhost:8000/api/v1/customers/check-availability?email=joao@example.com"
# Verificar CPF
curl "http://localhost:8000/api/v1/customers/check-availability?tax_id=12345678909"
# Verificar ambos
curl "http://localhost:8000/api/v1/customers/check-availability?email=joao@example.com&tax_id=12345678909"
Uso Recomendado¶
- Debounce: Aguarde 300-500ms após o usuário parar de digitar
- Validação local: Verifique formato antes de chamar API
- Feedback visual: Exiba ✅ ou ❌ ao lado do campo
- Bloquear submit: Desabilite botão se campo indisponível
🎯 POST /customers/signup¶
Cadastro de customer com verificacao de email (modelo freemium).
Fluxo atualizado (ADR-032 + ADR-058): O cadastro suporta dois modos controlados pela flag
verify_email_now(defaulttrue).
Mudanca de Fluxo (2026-02-09)
Por padrao (verify_email_now=true) o JWT so e retornado apos verificacao do email. O Passo 1 cria o customer mas NAO retorna token.
Modo B — Verificacao postergada (ADR-058)
Enviando verify_email_now=false, o cadastro retorna JWT imediato com email_verification_pending=true.
A verificacao do email pode ser feita depois via POST /customers/me/verify-email (autenticado).
Fluxo com Verificacao de Email¶
sequenceDiagram
participant Frontend
participant API
participant Resend as Resend (Email)
participant MongoDB
Note over Frontend: Passo 1 — Enviar dados (sem verification_code)
Frontend->>API: POST /customers/signup [{6 campos}]
API->>MongoDB: Verificar username/email unicos
API->>MongoDB: Criar customer (email_verified=false)
API->>Resend: Enviar codigo 6 digitos
API-->>Frontend: {customer_id, email, expires_in_minutes}
Note over Frontend: Exibir tela de codigo
Note over Frontend: Passo 2 — Verificar codigo (com verification_code)
Frontend->>API: POST /customers/signup [{6 campos + verification_code}]
API->>MongoDB: Validar hash do codigo
API->>MongoDB: email_verified=true
API-->>Frontend: {docs: [customer], info: {auth: {access_token}}}
Note over Frontend: JWT recebido — redirecionar
Request — Passo 1 (enviar dados, solicitar codigo)¶
POST /api/v1/customers/signup
Content-Type: application/json
[{
"username": "joao_silva",
"email": "joao@example.com",
"password": "senha_segura_123",
"full_name": "Joao da Silva",
"tax_id": "12345678909",
"phone": "+5511987654321",
"nationality": "BR"
}]
Request — Passo 2 (verificar codigo)¶
POST /api/v1/customers/signup
Content-Type: application/json
[{
"username": "joao_silva",
"email": "joao@example.com",
"password": "senha_segura_123",
"full_name": "Joao da Silva",
"tax_id": "12345678909",
"phone": "+5511987654321",
"nationality": "BR",
"verification_code": "482916"
}]
Campos:
| Campo | Tipo | Obrigatorio | Validacao | Descricao |
|---|---|---|---|---|
username |
string |
Sim | 3-50 chars, unico | Nome de usuario para login |
email |
string |
Sim | Email valido, unico | Email do customer |
password |
string |
Sim | Minimo 8 chars | Senha (hasheada no backend) |
full_name |
string |
Sim | 3-200 chars | Nome completo |
tax_id |
string |
Nao | 11 (CPF) ou 14 (CNPJ) digitos | CPF/CNPJ. Opcional no cadastro, obrigatorio para pagamento |
phone |
string |
Sim | +5511987654321 |
Telefone internacional |
nationality |
string |
Nao | 2-3 chars ISO 3166-1 | Nacionalidade (ex: BR, US, PRT) |
verification_code |
string |
Nao | 6 digitos (^\d{6}$) |
Omitir = solicitar codigo. Incluir = verificar. |
verify_email_now |
bool |
Nao | true (default) ou false |
true = fluxo 2 passos (sem JWT no Passo 1). false = retorna JWT imediato com email_verification_pending=true (verificar depois via POST /customers/me/verify-email). |
Campos opcionais no cadastro
tax_id, nationality e address sao opcionais no signup. O tax_id so e exigido no momento do pagamento (subscribe). Isso permite cadastro de clientes estrangeiros sem CPF.
Modo B — Resposta (200 OK, com verify_email_now=false)¶
{
"customer_id": "67abc...",
"email": "joao@example.com",
"email_verification_pending": true,
"message": "Conta criada. Verifique seu email mais tarde via POST /customers/me/verify-email.",
"expires_in_minutes": 15,
"auth": {
"access_token": "eyJhbGciOi...",
"token_type": "bearer",
"id_token": "67abc...",
"expiration": "2026-04-19T..."
}
}
🎯 POST /customers/me/verify-email¶
Verificacao postergada de email (Modo B). Endpoint autenticado para customers cadastrados com verify_email_now=false.
Request¶
POST /api/v1/customers/me/verify-email
Authorization: Bearer <access_token>
Content-Type: application/json
{ "code": "482917" }
Response — 200 OK¶
{
"message": "Email verificado com sucesso.",
"email_verified": true,
"email_verified_at": "2026-04-18T12:34:56Z"
}
Erros¶
| Status | Causa |
|---|---|
400 |
Email ja verificado / nenhum codigo ativo / codigo expirado / codigo incorreto |
404 |
Customer nao encontrado |
429 |
Maximo de tentativas (5) atingido — solicitar reenvio via POST /auth/resend-verification |
Response — Passo 1 (200 OK)¶
{
"customer_id": "507f1f77bcf86cd799439011",
"email": "joao@example.com",
"message": "Codigo de verificacao enviado para o email.",
"expires_in_minutes": 15
}
Sem JWT neste passo
O Passo 1 cria o customer com email_verified=false e nao retorna token JWT.
Response — Passo 2 (200 OK)¶
{
"docs": [
{
"_id": "507f1f77bcf86cd799439011",
"username": "joao_silva",
"email": "joao@example.com",
"user_type": "customer",
"full_name": "Joao da Silva",
"tax_id": "12345678909",
"phone": "+5511987654321",
"email_verified": true,
"address": null,
"current_subscription_id": null,
"created_at": "2026-02-09T10:30:00Z",
"updated_at": "2026-02-09T10:30:45Z"
}
],
"info": {
"auth": {
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer"
},
"message": "Email verificado com sucesso.",
"next_step": "/api/v1/customers/me/subscribe"
},
"total_docs": 1,
"qty_docs_page": 1,
"current_page": 0
}
Errors¶
409 - Username Already Exists¶
409 - Email Already Exists¶
400 - Codigo Expirado¶
400 - Codigo Incorreto¶
429 - Rate Limit (reenvio)¶
429 - Maximo de Tentativas¶
Regras de Verificacao
- Expiracao: 15 minutos
- Tentativas: Maximo 5 por codigo
- Reenvio: 1 por 60 segundos (via mesmo endpoint ou
POST /auth/resend-verification) - Seguranca: Codigo armazenado como SHA-256 hash, campos temporarios removidos apos verificacao
💳 POST /customers/me/subscribe¶
Assinatura posterior (customer já cadastrado).
Customer autenticado escolhe plano e efetua pagamento para liberar acesso a vídeos.
Fluxo Completo¶
sequenceDiagram
participant Frontend
participant API
participant Gateway as Gateway (Stripe/Asaas)
participant MongoDB
participant EmailService
Frontend->>API: POST /customers/me/subscribe
Note over Frontend: Authorization: Bearer <token>
API->>MongoDB: Verificar customer não tem assinatura ativa
API->>MongoDB: Verificar plano existe e está ativo
API->>Gateway: Criar assinatura
Gateway-->>API: subscription_id
API->>MongoDB: Salvar subscription
API->>MongoDB: Atualizar customer.current_subscription_id
API->>EmailService: Enviar email de confirmação
API-->>Frontend: {subscription, message}
Note over Frontend: Acesso a vídeos LIBERADO
Request¶
POST /api/v1/customers/me/subscribe
Authorization: Bearer eyJhbGci...
Content-Type: application/json
{
"plan_id": "plan-basic",
"payment_method": "credit_card",
"encrypted_card": "enc_Mzc0MjM3NDM3NDc3NDc0Njc0NjQ2NTY0NjU2MTY...",
"card_security_code": "123",
"address": {
"street": "Av. Paulista",
"number": "1000",
"complement": "Apto 123",
"locality": "Bela Vista",
"city": "São Paulo",
"region_code": "SP",
"country": "BRA",
"postal_code": "01310100"
}
}
Segurança: Dados de Pagamento
Os dados de pagamento NUNCA devem ser enviados em texto plano. Use os fluxos de tokenização dos gateways disponíveis (Stripe ou Asaas).
Campos obrigatórios (credit_card):
plan_id— Slug do plano (ex:"plan-basic")payment_method—"credit_card"address— Opcional se customer já cadastrou viaPUT /customers/me
Campos obrigatórios (pix):
plan_id— Slug do planopayment_method—"pix"amount— Valor anual em centavos, deve ser > 0 (ex:23880= R$ 238,80)address— Opcional
Integração de Pagamento
Para integração de cartão de crédito, utilize os gateways disponíveis:
- Stripe: Ver Stripe Integration
- Asaas: Ver Asaas Integration
Request PIX (alternativo)¶
POST /api/v1/customers/me/subscribe
Authorization: Bearer eyJhbGci...
Content-Type: application/json
{
"plan_id": "plan-basic",
"payment_method": "pix",
"amount": 23880,
"address": {
"street": "Av. Paulista",
"number": "1000",
"complement": "Apto 123",
"locality": "Bela Vista",
"city": "São Paulo",
"region_code": "SP",
"country": "BRA",
"postal_code": "01310100"
}
}
PIX = Assinatura Anual à Vista
PIX cria uma assinatura local com status=pending. Quando o pagamento via QR code é confirmado (webhook), o status muda para active e expires_at é definido como now + 1 ano.
Veja Guia do Frontend para fluxo completo com exemplos Dart/Flutter.
Recuperar QR code PIX
Se o customer abandonar o pagamento PIX, pode recuperar o QR code via GET /api/v1/customers/me/pix-qr. Ver secao abaixo.
Response (200 OK) — Credit Card¶
{
"subscription": {
"_id": "507f1f77bcf86cd799439012",
"customer_id": "507f1f77bcf86cd799439011",
"plan_id": "plan-basic",
"status": "active",
"payment_method": "credit_card",
"started_at": "2026-01-11T11:00:00Z",
"next_billing_date": "2026-02-11T00:00:00Z",
"expires_at": null
},
"message": "Subscription created successfully. You can now access premium content."
}
Response (200 OK) — PIX¶
{
"subscription": {
"_id": "507f1f77bcf86cd799439012",
"customer_id": "507f1f77bcf86cd799439011",
"plan_id": "plan-basic",
"status": "pending",
"payment_method": "pix",
"started_at": "2026-02-03T15:00:00Z",
"next_billing_date": null,
"expires_at": null
},
"pix": {
"qr_code_text": "00020126580014br.gov.bcb.pix0136...",
"qr_code_url": "https://...",
"expiration_date": "2026-02-03T15:30:00-03:00"
},
"message": "PIX subscription created. Scan QR code to pay. Subscription activates automatically after payment confirmation."
}
| Campo PIX | Descrição |
|---|---|
pix.qr_codes[0].text |
Codigo "copia e cola" para pagamento |
pix.qr_codes[0].links[0].href |
URL da imagem PNG do QR Code |
pix.expiration_date |
QR Code expira em 30 minutos |
Errors¶
400 - Already Has Active Subscription¶
{
"detail": "Customer already has an active subscription. Use PUT /customers/me/subscription/renew to change plan."
}
404 - Plan Not Found¶
400 - Inactive Plan¶
400 - Address Required¶
🔄 Recuperar QR Code PIX (Abandono)¶
Se o customer abandonar o pagamento PIX antes de escanear o QR code, pode recupera-lo via:
Request¶
Response (200 — Asaas)¶
{
"subscription_id": "69ca10ee68012dc25ceefdcb",
"status": "pending",
"gateway": "asaas",
"pix": {
"gateway": "asaas",
"payment_id": "pay_xxx",
"qr_code": "00020126580014br.gov.bcb.pix...",
"qr_code_image": "<base64 PNG>",
"expiration_date": "2026-03-31T03:00:00Z"
},
"message": "PIX QR code retrieved. Scan to complete payment."
}
Response (200 — Stripe)¶
{
"subscription_id": "69ca10ee68012dc25ceefdcb",
"status": "pending",
"gateway": "stripe",
"pix": {
"gateway": "stripe",
"payment_intent_id": "pi_3RChxWG6Qr6aGiCI",
"qr_code": "00020126580014br.gov.bcb.pix...",
"qr_code_image_url": "https://stripe.com/qr/...",
"expires_at": "2026-03-30T15:00:00Z",
"hosted_instructions_url": "https://payments.stripe.com/..."
},
"message": "PIX QR code retrieved. Scan to complete payment."
}
Errors¶
| HTTP | Cenario |
|---|---|
| 404 | Sem subscription pending |
| 400 | Subscription nao e PIX ou nao esta pending |
| 410 | QR code expirado (Stripe — expira em 30min) |
Flutter/Dart¶
/// Recuperar QR code PIX para subscription pending.
Future<Map<String, dynamic>?> recoverPixQr() async {
final resp = await http.get(
Uri.parse('$baseUrl/api/v1/customers/me/pix-qr'),
headers: {'Authorization': 'Bearer $token'},
);
if (resp.statusCode == 200) return jsonDecode(resp.body);
if (resp.statusCode == 404) return null; // sem pending
throw Exception('Erro: ${resp.body}');
}
Limpeza automatica (Asaas)
Subscriptions pending com mais de 24h sao auto-canceladas quando o customer tenta criar nova assinatura PIX. Isso desbloqueia o customer sem intervencao manual.
🔄 Fluxo Completo de Cadastro (Freemium)¶
Passo 1: Enviar dados e solicitar codigo¶
Customer cria conta com 6 campos (sem verification_code):
curl -X POST http://localhost:8000/api/v1/customers/signup \
-H "Content-Type: application/json" \
-d '[{
"username": "joao_silva",
"email": "joao@example.com",
"password": "senha123",
"full_name": "Joao da Silva",
"tax_id": "12345678909",
"phone": "+5511987654321"
}]'
Resultado:
- ✅ Conta criada (email_verified=false)
- ✅ Codigo de 6 digitos enviado por email
- ❌ Sem JWT (nao pode acessar endpoints protegidos)
Passo 2: Verificar codigo e receber JWT¶
Customer envia os mesmos dados + verification_code:
curl -X POST http://localhost:8000/api/v1/customers/signup \
-H "Content-Type: application/json" \
-d '[{
"username": "joao_silva",
"email": "joao@example.com",
"password": "senha123",
"full_name": "Joao da Silva",
"tax_id": "12345678909",
"phone": "+5511987654321",
"verification_code": "482916"
}]'
Resultado:
- ✅ Email verificado (email_verified=true)
- ✅ JWT token retornado
- ✅ Acesso ao painel liberado
- ❌ Videos bloqueados (GET /videos retorna 403 sem assinatura)
Passo 3: Assinatura Posterior¶
Customer decide assinar (via painel):
# Criar assinatura via Asaas ou Stripe
# Ver docs: asaas-integration.md ou stripe-integration.md
curl -X POST http://localhost:8000/api/v1/asaas/subscribe \
-H "Authorization: Bearer <TOKEN>" \
-H "Content-Type: application/json" \
-d '{ "plan_id": "plan-basic", ... }'
Resultado:
- ✅ Subscription ativa criada
- ✅ Vídeos liberados (GET /videos retorna 200)
🎯 POST /customers/signup-with-subscription¶
REMOVED: Este endpoint foi removido. Use o fluxo em duas etapas:
/customers/signup+/customers/me/subscribe.
👤 GET /customers/me¶
Buscar dados do customer autenticado.
A resposta é enriquecida com subscription_status e subscription_expires_at resolvidos a partir do documento da assinatura vinculada (via current_subscription_id). Isso permite ao frontend rotear pós-login sem precisar de uma segunda requisição a GET /customers/me/subscription.
Request¶
Response (200 OK)¶
{
"docs": [{
"_id": "507f1f77bcf86cd799439011",
"username": "joao_silva",
"email": "joao@example.com",
"user_type": "customer",
"full_name": "João da Silva",
"tax_id": "12345678909",
"phone": "+5511987654321",
"nationality": "BR",
"address": null,
"avatar_id": null,
"my_list": [],
"current_subscription_id": "507f1f77bcf86cd799439012",
"subscription_status": "active",
"subscription_expires_at": null,
"stripe_customer_id": "cus_abc123",
"asaas_customer_id": "cus_xyz789",
"email_verified": true,
"promo_code_ids": [],
"redeemed_promo_codes": [],
"bonus_tickets": 0,
"created_at": "2026-01-08T10:30:00Z",
"updated_at": "2026-01-08T10:30:00Z"
}]
}
Campos Enriquecidos (read-time)¶
| Campo | Tipo | Origem | Descrição |
|---|---|---|---|
subscription_status |
string \| null |
subscriptions.status (via current_subscription_id) |
Status atual da assinatura: active, pending, suspended, cancelled, expired. null = sem assinatura vinculada |
subscription_expires_at |
datetime \| null |
subscriptions.expires_at (via current_subscription_id) |
Data de expiração (ISO 8601 UTC). null para assinaturas com cobrança recorrente sem expiração definida (cartão), preenchida em PIX anual |
Importante: Esses campos são calculados em tempo de leitura — não são armazenados na collection
users. Securrent_subscription_idfornullou a subscription não existir, ambos retornamnull.
Lógica de roteamento sugerida (frontend)¶
final me = await getCustomerMe(token);
final status = me['docs'][0]['subscription_status'];
final expiresAt = me['docs'][0]['subscription_expires_at'];
if (status == 'active') {
// Liberar acesso a vídeos
navigateToHome();
} else if (status == 'pending') {
// PIX aguardando confirmação — exibir QR se disponível
navigateToPendingPayment();
} else {
// Sem assinatura ou cancelada/expirada — fluxo de assinatura
navigateToSubscribe();
}
✏️ PUT /customers/me¶
Atualizar dados do customer autenticado.
Request¶
PUT /api/v1/customers/me
Authorization: Bearer eyJhbGci...
Content-Type: application/json
[{
"full_name": "João Pedro da Silva",
"phone": "+5511999887766"
}]
Response (200 OK)¶
{
"docs": [{
"_id": "507f1f77bcf86cd799439011",
"full_name": "João Pedro da Silva",
"phone": "+5511999887766",
"my_list": [],
"updated_at": "2026-01-08T11:00:00Z"
}]
}
📋 GET /customers/me/subscription¶
Buscar assinatura ativa do customer autenticado.
Status Sincronizado no Login
O status de assinatura é automaticamente sincronizado com o gateway de pagamento (Stripe/Asaas) durante o login (POST /token). Após login bem-sucedido, este endpoint retorna o status real e atualizado.
Request¶
Response (200 OK)¶
{
"subscription": {
"_id": "507f1f77bcf86cd799439012",
"customer_id": "507f1f77bcf86cd799439011",
"username": "joao_silva",
"full_name": "João da Silva",
"plan_id": "plan-basic",
"gateway": "asaas",
"status": "active",
"payment_method": "credit_card",
"started_at": "2026-01-08T10:30:00Z",
"expires_at": null,
"last_payment_at": "2026-01-08T10:35:00Z",
"next_billing_date": "2026-02-08T00:00:00Z",
"payment_failures": 0,
"asaas_subscription_id": "sub_abc123",
"asaas_customer_id": "cus_xyz789",
"stripe_subscription_id": null,
"stripe_payment_intent_id": null,
"promo_code_id": null,
"promo_code_used": null,
"discount_first_month": null,
"original_amount": null,
"discount_applied": false,
"discount_needs_restore": false,
"total_refunded": 0,
"has_chargeback": false,
"chargeback_count": 0,
"last_chargeback_at": null,
"created_at": "2026-01-08T10:30:00Z",
"updated_at": "2026-01-08T10:35:00Z"
},
"message": "Active subscription retrieved successfully"
}
Campos¶
Identificação:
| Campo | Tipo | Descrição |
|---|---|---|
_id |
OID |
ID da assinatura |
customer_id |
OID |
ID do customer (FK users) |
username |
string \| null |
Username do customer (via $lookup) |
full_name |
string \| null |
Nome completo do customer (via $lookup) |
plan_id |
string |
ID interno do plano (slug) |
gateway |
"stripe" \| "asaas" |
Gateway de pagamento (default: asaas) |
Status e ciclo de vida:
| Campo | Tipo | Descrição |
|---|---|---|
status |
enum |
active, pending, suspended, cancelled, expired |
payment_method |
enum |
credit_card, boleto, pix |
started_at |
datetime |
Início da assinatura (UTC) |
expires_at |
datetime \| null |
Expiração (UTC). null = recorrente sem expiração; preenchido em PIX anual |
last_payment_at |
datetime \| null |
Último pagamento confirmado (UTC) |
next_billing_date |
datetime \| null |
Próxima cobrança (UTC). null para PIX (sem cobrança recorrente) |
payment_failures |
int |
Falhas de pagamento consecutivas (>=0). 3+ → suspende |
created_at |
datetime |
Criação do registro (UTC) |
updated_at |
datetime |
Última atualização (UTC) |
Identificadores de gateway:
| Campo | Tipo | Descrição |
|---|---|---|
asaas_subscription_id |
string \| null |
ID da subscription no Asaas (sub_xxx) |
asaas_customer_id |
string \| null |
ID do customer no Asaas (cus_xxx) |
stripe_subscription_id |
string \| null |
ID da subscription no Stripe (sub_xxx) |
stripe_price_id |
string \| null |
ID do price no Stripe |
stripe_payment_intent_id |
string \| null |
ID do PaymentIntent (PIX one-time) |
Promo code / desconto primeiro mês:
| Campo | Tipo | Descrição |
|---|---|---|
promo_code_id |
OID \| null |
ID do PromoCode usado na criação |
promo_code_used |
string \| null |
Código promo usado (string) |
discount_first_month |
float \| null |
Valor com desconto do 1º mês em reais (ex.: 19.9 = R$19,90) |
original_amount |
int \| null |
Valor original do plano em centavos antes do desconto |
discount_applied |
bool |
Se o desconto do 1º mês foi aplicado |
discount_needs_restore |
bool |
Se o valor da subscription precisa ser restaurado após o 1º pagamento (legado PIX — para cartão Asaas, ver ADR-054) |
Reembolso e chargeback:
| Campo | Tipo | Descrição |
|---|---|---|
total_refunded |
int |
Total reembolsado em centavos (>=0) |
has_chargeback |
bool |
Se houve chargeback detectado |
chargeback_count |
int |
Quantidade de chargebacks (>=0) |
last_chargeback_at |
datetime \| null |
Timestamp do último chargeback (UTC) |
Convenção de valores: -
discount_first_monthestá em reais (float) -original_amountetotal_refundedestão em centavos (int) - Esta divergência reflete a integração: Asaas usa reais; Stripe usa centavos
Errors¶
404 - No Subscription¶
🔄 PUT /customers/me/subscription/renew¶
Trocar o metodo de pagamento da assinatura ativa.
Como funciona
Este endpoint cancela a assinatura antiga no gateway e cria uma nova com o mesmo plano mas novo metodo de pagamento. O customer mantem acesso ininterrupto.
Apenas metodos recorrentes
Suporta credit_card e boleto. PIX nao e suportado (PIX usa pagamento unico anual via Orders API).
Request — Cartao de Credito¶
PUT /api/v1/customers/me/subscription/renew
Authorization: Bearer eyJhbGci...
Content-Type: application/json
{
"payment_method": "credit_card",
"encrypted_card": "enc_Mzc0MjM3NDM3NDc3NDc0Njc0NjQ2NTY0NjU2MTY...",
"card_security_code": "123"
}
Request — Boleto¶
PUT /api/v1/customers/me/subscription/renew
Authorization: Bearer eyJhbGci...
Content-Type: application/json
{
"payment_method": "boleto"
}
Campos do Body¶
| Campo | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
payment_method |
String |
Sim | "credit_card" ou "boleto" |
encrypted_card |
String |
Sim (credit_card/Asaas) | Cartao criptografado (Asaas). Para Stripe, use payment_method_id via flutter_stripe. |
card_security_code |
String |
Sim (credit_card) | CVV (3-4 digitos numericos, pattern ^\d{3,4}$) |
address |
Object |
Nao | Endereco de cobranca (atualiza no cadastro) |
Campos condicionais
encrypted_card e card_security_code sao obrigatorios apenas para credit_card.
Para boleto, envie apenas {"payment_method": "boleto"}.
Response (200 OK)¶
{
"subscription": {
"_id": "507f1f77bcf86cd799439099",
"customer_id": "507f1f77bcf86cd799439012",
"plan_id": "plan-basic",
"status": "active",
"payment_method": "credit_card",
"started_at": "2026-02-10T14:30:00Z",
"next_billing_date": "2026-03-10T00:00:00Z",
"expires_at": null,
"payment_failures": 0
},
"message": "Subscription renewed successfully. Payment method updated."
}
Fluxo Interno¶
sequenceDiagram
participant App as Flutter App
participant API as FDPlay API
participant GW as Gateway (Stripe/Asaas)
participant DB as MongoDB
App->>API: PUT /customers/me/subscription/renew
API->>DB: Buscar customer + subscription ativa
API->>DB: Buscar plano da subscription atual
API->>GW: Cancelar subscription antiga (best-effort)
Note over GW: Best-effort (falha nao bloqueia)
API->>GW: Criar nova subscription (mesmo plano, novo pagamento)
GW-->>API: {id: novo_id, status: ACTIVE}
API->>DB: Inserir nova subscription
API->>DB: Atualizar customer.current_subscription_id
API->>DB: Marcar subscription antiga como cancelled
API-->>App: {subscription, message}
Note over API: Email de confirmacao enviado
Errors¶
404 — Sem Assinatura Ativa¶
{
"error": {
"code": "RESOURCE_NOT_FOUND",
"message": "Customer does not have an active subscription to renew"
}
}
422 — Validacao de Payload¶
{
"detail": [
{
"type": "value_error",
"msg": "Value error, encrypted_card is required for credit_card payment method"
}
]
}
500 — Falha no Gateway¶
{
"error": {
"code": "INTERNAL_ERROR",
"message": "Subscription creation failed: no subscription ID returned"
}
}
Cancelamento best-effort
Se o cancelamento da subscription antiga no gateway falhar (timeout, erro de rede), o endpoint continua e cria a nova subscription normalmente. O erro e registrado nos logs para tratamento posterior.
❌ DELETE /customers/me/subscription¶
Cancelar assinatura ativa ou pendente (PIX). Funciona para todos os gateways e status.
Request¶
Comportamento por gateway/status¶
| Gateway | Status | Ação no gateway |
|---|---|---|
| Stripe (cartão) | active |
cancel_subscription (at_period_end=false) |
| Stripe (PIX) | pending |
cancel_payment_intent (cancela QR code) |
| Asaas (cartão/PIX) | active/pending |
cancel_subscription (delete no Asaas) |
Cancelar PIX pendente
Se o customer gerou um PIX mas quer usar outro meio de pagamento, basta chamar DELETE /customers/me/subscription para cancelar o PIX pendente. Depois pode criar nova assinatura normalmente.
Response (200 OK)¶
{
"message": "Subscription cancelled successfully",
"cancelled_at": "2026-01-08T13:00:00Z",
"note": "Subscription cancelled in gateway and local database"
}
Email: Um email de confirmação de cancelamento é enviado automaticamente.
🗑️ DELETE /customers/me — Deletar Conta (Self-Service)¶
Deletar conta do customer autenticado com cascade de subscription.
Requer confirmação de senha no body da request. Se o customer possui assinatura ativa, ela é cancelada no gateway de pagamento e marcada como
cancelledno MongoDB antes da exclusão do customer.
Fluxo de Cascade¶
sequenceDiagram
participant Frontend
participant API
participant GW as Gateway (Stripe/Asaas)
participant MongoDB
Frontend->>API: DELETE /customers/me {password}
Note over Frontend: Authorization: Bearer <token>
API->>API: Verificar senha atual (Argon2)
alt Senha incorreta
API-->>Frontend: 400 VALIDATION_ERROR
else Senha correta
API->>MongoDB: Buscar customer
alt Tem assinatura ativa
API->>MongoDB: Buscar subscription
API->>GW: Cancel subscription (best-effort)
GW-->>API: OK / erro (ignorado)
API->>MongoDB: Marcar subscription status=cancelled
end
API->>MongoDB: Deletar customer document
API-->>Frontend: 200 {message}
end
Request¶
DELETE /api/v1/customers/me
Authorization: Bearer eyJhbGci...
Content-Type: application/json
{
"password": "minha_senha_atual"
}
| Campo | Tipo | Obrigatório | Validação | Descrição |
|---|---|---|---|---|
password |
string |
✅ Sim | mínimo 1 char | Senha atual para confirmação |
Exemplo cURL:
curl -X DELETE "http://localhost:8000/api/v1/customers/me" \
-H "Authorization: Bearer eyJhbGci..." \
-H "Content-Type: application/json" \
-d '{"password": "minha_senha_atual"}'
Response (200 OK)¶
{
"message": "Conta excluida com sucesso.",
"details": {
"customer_deleted": true,
"subscription_cancelled": true
}
}
O campo details contém informações do cascade (se havia assinatura ativa, se foi cancelada no gateway, etc.).
Errors¶
400 — Senha Incorreta¶
404 — Customer Não Encontrado¶
Exemplo Flutter/Dart¶
import 'dart:convert';
import 'package:http/http.dart' as http;
class AccountService {
static const String baseUrl = 'https://fdplay-api.infraifd.com/api/v1';
/// Deleta a conta do customer autenticado.
///
/// Requer confirmação de senha. Cancela assinatura ativa (cascade).
/// Após sucesso, o token JWT torna-se inválido — redirecionar ao login.
Future<void> deleteAccount({
required String token,
required String password,
}) async {
final response = await http.delete(
Uri.parse('$baseUrl/customers/me'),
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
},
body: jsonEncode({'password': password}),
);
if (response.statusCode != 200) {
final body = jsonDecode(response.body);
final message = body['error']?['message'] ?? body['detail'] ?? 'Erro ao deletar conta';
throw Exception(message);
}
}
}
// Uso:
final service = AccountService();
await service.deleteAccount(token: token, password: 'minha_senha');
// Limpar token e redirecionar ao login
final prefs = await SharedPreferences.getInstance();
await prefs.clear();
Navigator.pushReplacementNamed(context, '/login');
Ação Irreversível
A exclusão da conta é permanente. O frontend deve exibir um modal de confirmação antes de chamar este endpoint:
- Exibir aviso claro: "Esta ação não pode ser desfeita"
- Se houver assinatura ativa, informar que será cancelada
- Exigir digitação da senha atual
- Após sucesso, limpar token local e redirecionar ao login
🔧 Admin Endpoints¶
GET /customers (Admin)¶
Listar todos os customers com filtros.
GET /api/v1/customers?query={"current_subscription_id":{"$ne":null}}
Authorization: Bearer eyJhbGci... (admin token)
PUT /customers (Admin)¶
Atualizar qualquer customer.
DELETE /customers (Admin)¶
Deletar customer com cascade de subscription. Se o customer possui assinatura ativa, ela é cancelada no gateway de pagamento e marcada como cancelled no MongoDB antes da exclusão.
🖼️ Avatar do Customer¶
O avatar usa um endpoint dedicado (POST /me/avatar) que faz tudo em uma única chamada: valida formato/tamanho, armazena no GridFS, atualiza o avatar_id no perfil e remove automaticamente o avatar antigo.
Documentação Completa
Para exemplos Flutter/Dart completos (Service, Widget, diagrama), veja Perfil — Avatar.
Fluxo de Inserção ou Atualização¶
flowchart TD
A["Usuário seleciona imagem"] --> B{"Validar no frontend"}
B -->|"Formato inválido"| C["Exibir erro: use JPEG, PNG ou WebP"]
B -->|"> 5 MB"| D["Exibir erro: imagem muito grande"]
B -->|"OK"| E["POST /me/avatar<br/>(multipart/form-data)"]
E --> F{"Backend valida"}
F -->|"Erro 400"| G["Retorna mensagem de erro"]
F -->|"OK"| H["Armazena nova imagem no GridFS"]
H --> I["Atualiza avatar_id no documento do usuário"]
I --> J{"Tinha avatar anterior?"}
J -->|"Sim"| K["Remove avatar antigo do GridFS"]
J -->|"Não"| L["Retorna avatar_id + mensagem"]
K --> L
Passo a passo:
- Frontend seleciona a imagem e valida formato (JPEG/PNG/WebP) e tamanho (max 5 MB)
- Envia
POST /api/v1/me/avatarcommultipart/form-data - Backend valida, armazena no GridFS, atualiza
avatar_idno perfil - Se já existia avatar anterior, o backend remove automaticamente o antigo do GridFS
- Retorna o novo
avatar_idpara uso emGET /avatars/{avatar_id}
Endpoints de Avatar¶
| Ação | Método | Endpoint | Auth | Descrição |
|---|---|---|---|---|
| Upload/Atualizar | POST |
/api/v1/me/avatar |
JWT | Envia imagem (substitui anterior automaticamente) |
| Exibir | GET |
/api/v1/avatars/{avatar_id} |
Pública | Retorna imagem binária (usar em Image.network) |
| Remover | DELETE |
/api/v1/me/avatar |
JWT | Remove do GridFS e limpa avatar_id |
Upload — Request¶
POST /api/v1/me/avatar
Authorization: Bearer <TOKEN_JWT>
Content-Type: multipart/form-data
file: <arquivo_imagem>
Upload — Response (200 OK)¶
Exibir Avatar¶
URL pública — basta usar o avatar_id retornado no upload ou em GET /customers/me:
Retorna a imagem binária com Content-Type correto (image/jpeg, image/png ou image/webp).
Remover Avatar¶
Erros¶
| Status | Mensagem | Causa |
|---|---|---|
400 |
Tipo de arquivo não permitido: image/gif. Use JPEG, PNG ou WebP. |
Formato inválido |
400 |
Arquivo excede o limite de 5 MB. |
Arquivo muito grande |
404 |
Nenhum avatar encontrado. |
Tentou remover sem ter avatar |
404 |
Avatar não encontrado. |
file_id inválido no GET /avatars/{id} |
Exemplo Flutter/Dart¶
import 'dart:io';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:flutter/material.dart';
/// Upload de avatar — endpoint dedicado, uma única chamada.
Future<String> uploadAvatar(String baseUrl, String token, File imageFile) async {
final request = http.MultipartRequest(
'POST',
Uri.parse('$baseUrl/api/v1/me/avatar'),
);
request.headers['Authorization'] = 'Bearer $token';
request.files.add(await http.MultipartFile.fromPath('file', imageFile.path));
final streamedResponse = await request.send();
final response = await http.Response.fromStream(streamedResponse);
final body = jsonDecode(response.body);
if (response.statusCode == 200) {
return body['avatar_id']; // Usar em GET /avatars/{avatar_id}
}
throw Exception(body['detail'] ?? 'Erro ao fazer upload do avatar');
}
/// Remover avatar.
Future<void> deleteAvatar(String baseUrl, String token) async {
final response = await http.delete(
Uri.parse('$baseUrl/api/v1/me/avatar'),
headers: {'Authorization': 'Bearer $token'},
);
if (response.statusCode != 200) {
final body = jsonDecode(response.body);
throw Exception(body['detail'] ?? 'Erro ao remover avatar');
}
}
/// Exibir avatar — URL pública, sem autenticação.
Widget buildAvatar(String baseUrl, String? avatarId) {
if (avatarId == null) {
return const CircleAvatar(radius: 50, child: Icon(Icons.person, size: 50));
}
return CircleAvatar(
radius: 50,
backgroundImage: NetworkImage('$baseUrl/api/v1/avatars/$avatarId'),
);
}
📝 Notas de Implementação¶
Rollback em Erros¶
Se a criação da subscription no gateway falhar, o customer criado no MongoDB é automaticamente deletado (rollback).
Emails Transacionais¶
- Signup: Email de boas-vindas com detalhes do plano
- Cancelamento: Email de confirmação
Segurança¶
- Senhas são hasheadas com Argon2 antes de salvar
- JWT expira em 12 horas (configurável)
- Gateways de pagamento usam HTTPS + idempotency keys
🔗 Ver Também¶
- Plans API — Listar planos disponíveis
- Admin Dashboard — Gerenciar subscriptions
- Webhooks — Eventos de pagamento