Skip to content

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_required para 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)

{
  "message": "Password changed successfully",
  "password_reset_required": false
}

Errors

400 - Senha Atual Incorreta

{
  "detail": "Current password is incorrect"
}

404 - Usuário Não Encontrado

{
  "detail": "User not found"
}

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-emailPOST /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

{
  "error": {
    "code": "DUPLICATE_KEY",
    "message": "Username already exists",
    "field": "username"
  }
}

400 - Email Bloqueado

{
  "detail": "Email nao pode ser alterado diretamente. Use POST /customers/me/change-email."
}

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

GET /api/v1/customers/check-availability?email=joao@example.com&tax_id=12345678909

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)

{
  "email_available": true,
  "tax_id_available": false
}
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

{
  "detail": "At least one of email or tax_id must be provided"
}

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

  1. Debounce: Aguarde 300-500ms após o usuário parar de digitar
  2. Validação local: Verifique formato antes de chamar API
  3. Feedback visual: Exiba ✅ ou ❌ ao lado do campo
  4. 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 (default true).

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

{
  "detail": "Username already exists"
}

409 - Email Already Exists

{
  "detail": "Email already exists"
}

400 - Codigo Expirado

{
  "detail": "Codigo expirado. Reenvie o codigo."
}

400 - Codigo Incorreto

{
  "detail": "Codigo incorreto. 3 tentativa(s) restante(s)."
}

429 - Rate Limit (reenvio)

{
  "detail": "Aguarde 45 segundos para reenviar o codigo."
}

429 - Maximo de Tentativas

{
  "detail": "Maximo de tentativas atingido. Reenvie o codigo."
}

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"
  • addressOpcional se customer já cadastrou via PUT /customers/me

Campos obrigatórios (pix):

  • plan_id — Slug do plano
  • payment_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:

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

{
  "detail": "Plan not found: plan-basic"
}

400 - Inactive Plan

{
  "detail": "Plan is not active: plan-basic"
}

400 - Address Required

{
  "detail": "Address is required. Provide address in request or update customer profile first."
}

🔄 Recuperar QR Code PIX (Abandono)

Se o customer abandonar o pagamento PIX antes de escanear o QR code, pode recupera-lo via:

Request

GET /api/v1/customers/me/pix-qr
Authorization: Bearer <customer_token>

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

GET /api/v1/customers/me
Authorization: Bearer eyJhbGci...

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. Se current_subscription_id for null ou a subscription não existir, ambos retornam null.

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

GET /api/v1/customers/me/subscription
Authorization: Bearer eyJhbGci...

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_month está em reais (float) - original_amount e total_refunded estão em centavos (int) - Esta divergência reflete a integração: Asaas usa reais; Stripe usa centavos

Errors

404 - No Subscription

{
  "detail": "No active subscription found for this customer"
}

🔄 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

DELETE /api/v1/customers/me/subscription
Authorization: Bearer eyJhbGci...

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 cancelled no 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

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Senha incorreta.",
    "field": "password"
  }
}

404 — Customer Não Encontrado

{
  "error": {
    "code": "NOT_FOUND",
    "message": "Customer nao 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:

  1. Exibir aviso claro: "Esta ação não pode ser desfeita"
  2. Se houver assinatura ativa, informar que será cancelada
  3. Exigir digitação da senha atual
  4. 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:

  1. Frontend seleciona a imagem e valida formato (JPEG/PNG/WebP) e tamanho (max 5 MB)
  2. Envia POST /api/v1/me/avatar com multipart/form-data
  3. Backend valida, armazena no GridFS, atualiza avatar_id no perfil
  4. Se já existia avatar anterior, o backend remove automaticamente o antigo do GridFS
  5. Retorna o novo avatar_id para uso em GET /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)

{
  "avatar_id": "507f1f77bcf86cd799439011",
  "message": "Avatar atualizado com sucesso."
}

Exibir Avatar

URL pública — basta usar o avatar_id retornado no upload ou em GET /customers/me:

GET /api/v1/avatars/507f1f77bcf86cd799439011

Retorna a imagem binária com Content-Type correto (image/jpeg, image/png ou image/webp).

Remover Avatar

DELETE /api/v1/me/avatar
Authorization: Bearer <TOKEN_JWT>
{
  "message": "Avatar removido com sucesso."
}

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