openapi: 3.1.0

info:
  title: "Notreve SMS — API Pública"
  version: "3.0"
  description: |
    API REST de Notreve SMS. Cobre autenticação, envio unitário e em lote,
    histórico de mensagens, conta & saldo, chaves de API e verificação 2FA.

    **Autenticação**: Bearer token em `Authorization: Bearer {token}`.
    Obtenha o token via `POST /v3/auth/login` ou no painel em Configurações → Token da API.

    **Chaves de API**: além do token master, você pode criar Chaves de API com saldo próprio.
    Cada chave funciona como uma sub-conta independente — autentique com o token da chave
    nos endpoints de envio (`/v3/sms/send`).

    **Formato de resposta**:
    ```json
    { "status": true,  "message": "...", "data": { ... } }
    { "status": false, "message": "...", "data": null }
    ```

    **Rate limits**:
    - `POST /v3/sms/send` — 60 req/min por usuário, 120 req/min por IP
    - `POST /v3/sms/bulk` — 10 req/min por usuário
    - Excedido → HTTP 429 com header `Retry-After`

    **Webhooks**: passe `webhook_url` no envio para receber notificações POST.
    Dois tipos de evento são entregues na mesma URL:
    - **Atualização de status** — quando o SMS muda para `sent`, `delivered`, `error`, etc.
    - **Resposta recebida** (`sms_reply`) — quando o destinatário responde ao SMS (requer operadora com suporte a respostas).

    Veja os payloads completos na seção **Webhooks** ao final desta documentação.

servers:
  - url: "https://api.sms.notreve.com.br/v3"
    description: API autenticada

tags:
  - name: Saúde
    description: Health check sem autenticação.
  - name: Autenticação
    description: Login, esqueci senha e reset de senha.
  - name: SMS
    description: Envio unitário, lote e histórico de mensagens.
  - name: Conta
    description: Saldo e transferência de créditos.
  - name: Chaves de API
    description: Gerenciamento de chaves adicionais com saldo próprio.
  - name: 2FA
    description: Envio e verificação de PIN por SMS.
  - name: Painel
    description: Endpoints do painel do cliente — mensagens, extrato e perfil. Requerem autenticação do usuário.
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: Token obtido em `POST /v3/auth/login`.

  schemas:

    SuccessEnvelope:
      type: object
      properties:
        status:
          type: boolean
          example: true
        message:
          type: string
          example: Operação realizada com sucesso.
        data:
          description: Payload da resposta (varia por rota).

    ErrorEnvelope:
      type: object
      properties:
        status:
          type: boolean
          example: false
        message:
          type: string
          example: Mensagem de erro.
        data:
          nullable: true
          example: null

    Message:
      type: object
      properties:
        message_id:
          type: string
          example: a3f8d2c1b9e7f4d5c6b7
        destination_number:
          type: string
          example: "5511999999999"
        message:
          type: string
          example: Seu código é 4821.
        tag:
          type: string
          nullable: true
          example: otp
        status:
          type: string
          enum: [queued, processed, sent, delivered, error, blocked, cancelled]
          example: delivered
        created_at:
          type: string
          format: date-time
        sent_at:
          type: string
          format: date-time
          nullable: true
        delivered_at:
          type: string
          format: date-time
          nullable: true
        error_at:
          type: string
          format: date-time
          nullable: true

    ApiKey:
      type: object
      properties:
        id:
          type: integer
          example: 1
        name:
          type: string
          example: Integração Loja X
        description:
          type: string
          nullable: true
          example: Envios da loja online
        token_prefix:
          type: string
          example: a1b2c3d4
        balance_sms:
          type: integer
          example: 200
        status:
          type: string
          enum: [active, blocked, cancelled]
          example: active
        activation_cost:
          type: integer
          example: 0
        created_at:
          type: string
          format: date-time
        blocked_at:
          type: string
          format: date-time
          nullable: true
        cancelled_at:
          type: string
          format: date-time
          nullable: true

  parameters:
    keyId:
      name: id
      in: path
      required: true
      schema:
        type: integer
      example: 1

    page:
      name: page
      in: query
      schema:
        type: integer
        default: 1
      example: 1

    perPage:
      name: per_page
      in: query
      schema:
        type: integer
        default: 50
      example: 50

    dtIni:
      name: dt_ini
      in: query
      schema:
        type: string
        format: date
      example: "2025-06-01"

    dtFim:
      name: dt_fim
      in: query
      schema:
        type: string
        format: date
      example: "2025-06-30"

  responses:
    Unauthorized:
      description: Token ausente ou inválido.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          example:
            status: false
            message: Token not authorized.
            data: null
    NotFound:
      description: Recurso não encontrado.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    UnprocessableEntity:
      description: Erro de validação.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    TooManyRequests:
      description: Rate limit excedido.
      headers:
        Retry-After:
          schema:
            type: integer
          description: Segundos até o próximo request permitido.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          example:
            status: false
            message: Rate limit exceeded. Try again later.
            data: null

# ── Paths ─────────────────────────────────────────────────────────────────────
paths:

  # ────────────── Saúde ────────────────────────────────────────────────────────
  /health:
    get:
      summary: Health check da API
      description: |
        Probe sem autenticação. Retorna 200 quando todos os serviços estão operacionais.
      tags: [Saúde]
      security: []
      responses:
        "200":
          description: Serviço saudável.
          content:
            application/json:
              example:
                status: true
                message: Todos os serviços operacionais
        "503":
          description: Serviço degradado.
          content:
            application/json:
              example:
                status: false
                message: Um ou mais serviços indisponíveis

  # ────────────── Autenticação ─────────────────────────────────────────────────
  /auth/login:
    post:
      summary: Autenticar e obter token
      tags: [Autenticação]
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email:
                  type: string
                  format: email
                  example: voce@empresa.com
                password:
                  type: string
                  example: suaSenha
      responses:
        "200":
          description: Login realizado. Retorna token e dados do usuário.
          content:
            application/json:
              example:
                status: true
                message: Login realizado com sucesso.
                data:
                  token: "notreve_sms_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
                  user:
                    id: 1
                    name: João Silva
                    email: voce@empresa.com
                    balance_sms: 5000
                    profile: admin
        "401":
          description: Credenciais inválidas.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'

  /auth/channels:
    get:
      summary: Canais de recuperação de senha ativos
      description: Retorna quais canais estão habilitados no tenant para recuperação de senha. Sem autenticação.
      tags: [Autenticação]
      security: []
      responses:
        "200":
          description: Lista de canais disponíveis.
          content:
            application/json:
              example:
                status: true
                message: OK
                data:
                  channels: [email, sms]

  /auth/forgot:
    post:
      summary: Solicitar código de recuperação de senha
      description: |
        Gera um código OTP de 6 dígitos e o envia pelo canal escolhido.
        Retorna um `session` token para ser usado em `/auth/verify-code`.
        A resposta é sempre `status: true` para não revelar se o e-mail existe.
      tags: [Autenticação]
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email:
                  type: string
                  format: email
                  example: voce@empresa.com
                channel:
                  type: string
                  enum: [email, sms, whatsapp]
                  example: email
                  description: Canal para receber o código. Padrão — primeiro canal ativo.
      responses:
        "200":
          description: Código enviado (resposta idêntica mesmo se e-mail não existir).
          content:
            application/json:
              example:
                status: true
                message: Se esse e-mail estiver cadastrado, você receberá o código em breve.
                data:
                  session: a3f8d2c1b9e7f4d5a3f8d2c1b9e7f4d5a3f8d2c1b9e7f4d5a3f8d2c1b9e7f4

  /auth/verify-code:
    post:
      summary: Validar código OTP de recuperação
      description: |
        Valida o código de 6 dígitos recebido via `POST /auth/forgot`.
        Em caso de sucesso retorna o `token` para ser usado em `POST /auth/reset`.
      tags: [Autenticação]
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [session, code]
              properties:
                session:
                  type: string
                  example: a3f8d2c1b9e7f4d5a3f8d2c1b9e7f4d5a3f8d2c1b9e7f4d5a3f8d2c1b9e7f4
                code:
                  type: string
                  example: "123456"
      responses:
        "200":
          description: Código válido — use `token` em `POST /auth/reset`.
          content:
            application/json:
              example:
                status: true
                message: Código verificado.
                data:
                  token: a3f8d2c1b9e7f4d5a3f8d2c1b9e7f4d5a3f8d2c1b9e7f4d5a3f8d2c1b9e7f4
        "422":
          description: Código incorreto.
          $ref: '#/components/responses/UnprocessableEntity'
        "410":
          description: Sessão inválida ou expirada.
          $ref: '#/components/responses/UnprocessableEntity'

  /auth/logout:
    post:
      summary: Registrar logout
      description: Registra o evento de logout na trilha de auditoria. Deve ser chamado antes de descartar o token.
      tags: [Autenticação]
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Logout registrado.
          content:
            application/json:
              example:
                status: true
                message: Logout registrado.
                data: null
        "401":
          $ref: '#/components/responses/Unauthorized'

  /auth/reset:
    get:
      summary: Verificar validade do token de reset
      tags: [Autenticação]
      security: []
      parameters:
        - name: token
          in: query
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Token válido.
          content:
            application/json:
              example:
                status: true
                message: Token válido.
                data: null
        "422":
          description: Token inválido ou expirado.
          $ref: '#/components/responses/UnprocessableEntity'
    post:
      summary: Aplicar nova senha
      tags: [Autenticação]
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token, password]
              properties:
                token:
                  type: string
                  example: reset_token_aqui
                password:
                  type: string
                  example: novaSenhaSegura
      responses:
        "200":
          description: Senha redefinida.
          content:
            application/json:
              example:
                status: true
                message: Senha redefinida com sucesso.
                data: null
        "422":
          $ref: '#/components/responses/UnprocessableEntity'

  # ────────────── SMS ────────────────────────────────────────────────────────
  /sms/send:
    post:
      summary: Enviar SMS unitário
      description: |
        Enfileira uma mensagem SMS. Debita 1 crédito imediatamente e retorna o `message_id`.
        Aceita token master ou Chave de API (com saldo próprio).

        **Formato da mensagem**

        - Limite de **160 caracteres**. Acentos (ã, ç, é) contam como 1 caractere cada.
        - **Linha unica obrigatoria:** quebra de linha, tabulacao e sequencias
          literais `\\n`, `\\r` ou `\\t` sao normalizadas para espaco antes
          de gravar/enviar. Caracteres de controle sao removidos.
          O limite de 160 e aplicado sobre o texto normalizado.
        - **Não use Markdown:** SMS é texto puro. `[link](url)` e `**negrito**` chegam exatamente
          assim, sem renderização.
        - **URLs:** incluem no limite de 160 chars. Encurte antes de enviar se a mensagem for longa.
        - **Emojis:** fazem a operadora usar codificação Unicode, reduzindo o limite efetivo a
          70 caracteres. Evite em mensagens transacionais.

        **Webhook de status**: informe `webhook_url` para receber uma notificação POST
        quando o status do SMS mudar. Veja o schema do payload em
        [Webhooks de status](#section/Webhooks-de-status).
      tags: [SMS]
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [destination_number, message]
              properties:
                destination_number:
                  type: string
                  example: "5511999999999"
                  description: DDI + DDD + número. Brasil = 55.
                message:
                  type: string
                  maxLength: 160
                  example: "Ola, Joao! Seu pedido foi confirmado. Retirada disponivel a partir das 14h."
                  description: >
                    Texto do SMS. Limite de 160 caracteres — letras com acento (ã, ç, é) contam normalmente
                    como 1 caractere cada.

                    **Linha unica obrigatoria:** quebra de linha, tabulacao e sequencias
                    literais `\\n`, `\\r` ou `\\t` sao normalizadas para espaco antes
                    de gravar/enviar. Caracteres de controle sao removidos.
                    O limite de 160 e aplicado sobre o texto normalizado.

                    **Evite:** formatação Markdown (`[texto](url)`, `**negrito**`), pois SMS não renderiza
                    nenhuma marcação — tudo chega como texto puro. URLs longas devem ser encurtadas antes
                    do envio para não comprometer o limite de 160 chars.

                    **Emojis:** reduzem o limite efetivo a 70 chars por parte. Prefira não usá-los em
                    mensagens transacionais onde o espaço é crítico.
                tag:
                  type: string
                  nullable: true
                  example: otp
                  description: Etiqueta para agrupamento e filtros.
                webhook_url:
                  type: string
                  format: uri
                  nullable: true
                  example: https://sua-api.com/sms/callback
                  description: URL que receberá POST quando o status mudar.
      responses:
        "202":
          description: Mensagem enfileirada.
          content:
            application/json:
              example:
                status: true
                message: Message queued successfully.
                data:
                  id: a3f8d2c1b9e7f4d5
                  status: queued
                  created_at: "2025-06-01 10:30:00"
        "400":
          description: Parâmetros inválidos.
          $ref: '#/components/responses/UnprocessableEntity'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "402":
          description: Saldo insuficiente.
          content:
            application/json:
              example:
                status: false
                message: User does not have sufficient SMS credits.
                data: null
        "422":
          description: Bloqueado por filtro de conteúdo.
          content:
            application/json:
              example:
                status: false
                message: Message blocked by content filter.
                data:
                  filter: PROIBIDO
        "429":
          $ref: '#/components/responses/TooManyRequests'

  /sms/bulk:
    post:
      summary: Envio em lote (assíncrono)
      description: |
        Enfileira até 10.000 SMS em uma chamada. Debita créditos antecipadamente.
        Aceita token master ou Chave de API (com saldo próprio).

        `tag` é obrigatória para identificar o lote e filtrar o histórico depois.

        **Formato da mensagem**

        - Limite de **160 caracteres**. Acentos (ã, ç, é) contam como 1 caractere cada.
        - **Linha unica obrigatoria:** quebra de linha, tabulacao e sequencias
          literais `\\n`, `\\r` ou `\\t` sao normalizadas para espaco antes
          de gravar/enviar. Caracteres de controle sao removidos.
          O limite de 160 e aplicado sobre o texto normalizado.
        - **Não use Markdown:** SMS é texto puro — `[link](url)` chega exatamente assim.
        - **URLs longas:** encurte antes do envio para não comprimir o espaço da mensagem.
        - **Emojis:** reduzem o limite efetivo a 70 chars. Evite em disparos em massa.
      tags: [SMS]
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [destination_numbers, message, tag]
              properties:
                destination_numbers:
                  type: array
                  items:
                    type: string
                  maxItems: 10000
                  example: ["5511999999999", "5521988887777"]
                  description: Array de números celulares brasileiros (55 + DDD + 9XXXXXXXX). Fixos e inválidos são rejeitados. Duplicatas são removidas automaticamente.
                message:
                  type: string
                  maxLength: 160
                  example: "Promocao especial! 30% OFF hoje. Acesse: meusite.com.br/promo"
                  description: >
                    Texto do SMS. Limite de 160 caracteres — letras com acento (ã, ç, é) contam normalmente
                    como 1 caractere cada.

                    **Linha unica obrigatoria:** quebra de linha, tabulacao e sequencias
                    literais `\\n`, `\\r` ou `\\t` sao normalizadas para espaco antes
                    de gravar/enviar. Caracteres de controle sao removidos.
                    O limite de 160 e aplicado sobre o texto normalizado.

                    **Evite:** formatação Markdown, URLs longas sem encurtar e emojis em disparos em massa
                    (emojis reduzem o limite efetivo a 70 chars por parte).
                tag:
                  type: string
                  example: campanha-junho
                  description: Etiqueta obrigatória para identificar o lote.
                webhook_url:
                  type: string
                  format: uri
                  nullable: true
                  example: https://sua-api.com/sms/bulk-status
      responses:
        "202":
          description: Lote enfileirado.
          content:
            application/json:
              example:
                status: true
                message: Batch queued successfully.
                data:
                  queued: 2
                  skipped: 0
                  invalid: 0
                  tag: campanha-junho
        "400":
          $ref: '#/components/responses/UnprocessableEntity'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "402":
          description: Saldo insuficiente.
          content:
            application/json:
              example:
                status: false
                message: Insufficient credits for batch.
                data: null
        "422":
          description: Bloqueado por filtro de conteúdo.
          content:
            application/json:
              example:
                status: false
                message: Message blocked by content filter.
                data:
                  filter: PALAVRA_PROIBIDA
        "429":
          $ref: '#/components/responses/TooManyRequests'

  /sms/list:
    get:
      summary: Histórico de mensagens
      tags: [SMS]
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/page'
        - name: per_page
          in: query
          schema:
            type: integer
            default: 50
            maximum: 100
        - name: status
          in: query
          schema:
            type: string
            enum: [queued, processed, sent, delivered, error, blocked, cancelled]
            description: Filtrar por status. `processing` e `invalid` são status internos não retornados nesta rota. `cancelled` indica SMS cancelado pelo admin antes do envio (crédito estornado).
        - name: tag
          in: query
          schema:
            type: string
          example: otp
        - name: q
          in: query
          schema:
            type: string
          description: Busca por número ou texto da mensagem.
        - $ref: '#/components/parameters/dtIni'
        - $ref: '#/components/parameters/dtFim'
      responses:
        "200":
          description: Lista de mensagens.
          content:
            application/json:
              example:
                status: true
                data:
                  messages:
                    - message_id: a3f8d2c1b9e7f4d5
                      destination_number: "5511999999999"
                      message: Seu código é 4821.
                      tag: otp
                      status: delivered
                      created_at: "2025-06-01 10:30:00"
                      sent_at: "2025-06-01 10:30:05"
                      delivered_at: "2025-06-01 10:30:12"
                      error_at: null
                  total: 1
                  page: 1
                  per_page: 50
                  total_pages: 1
        "401":
          $ref: '#/components/responses/Unauthorized'

  # ────────────── Conta ─────────────────────────────────────────────────────
  /accounts/balance:
    get:
      summary: Consultar saldo de créditos
      tags: [Conta]
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Saldo da conta.
          content:
            application/json:
              example:
                status: true
                data:
                  balance_sms: 4999
                  unlimited_balance: false
        "401":
          $ref: '#/components/responses/Unauthorized'

  # ────────────── Chaves de API ──────────────────────────────────────────────
  /keys:
    get:
      summary: Listar chaves de API
      tags: [Chaves de API]
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/page'
        - name: per_page
          in: query
          schema:
            type: integer
            default: 20
            maximum: 50
        - name: status
          in: query
          schema:
            type: string
            enum: [active, blocked, cancelled]
        - name: q
          in: query
          schema:
            type: string
          description: Busca por nome ou descrição.
        - $ref: '#/components/parameters/dtIni'
        - $ref: '#/components/parameters/dtFim'
      responses:
        "200":
          description: Lista de chaves.
          content:
            application/json:
              example:
                status: true
                data:
                  keys:
                    - id: 1
                      name: Integração Loja X
                      description: Envios da loja online
                      token_prefix: a1b2c3d4
                      balance_sms: 200
                      status: active
                      activation_cost: 0
                      created_at: "2025-06-01 09:00:00"
                  total: 1
                  page: 1
                  per_page: 20
                  total_pages: 1
                  activation_cost: 0
                  status_totals:
                    active: 1
                    blocked: 0
                    cancelled: 0
        "401":
          $ref: '#/components/responses/Unauthorized'

    post:
      summary: Criar chave de API
      description: |
        Cria uma nova chave com saldo próprio. O campo `token` da resposta é exibido
        **apenas uma vez** — guarde-o imediatamente.

        Se o tenant cobrar por ativação (`activation_cost > 0`), o custo é debitado
        da conta principal no ato da criação.
      tags: [Chaves de API]
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                  maxLength: 100
                  example: Integração Loja X
                description:
                  type: string
                  maxLength: 255
                  nullable: true
                  example: Envios da loja online
      responses:
        "201":
          description: Chave criada. O token só é retornado nesta resposta.
          content:
            application/json:
              example:
                status: true
                message: Chave criada com sucesso.
                data:
                  id: 1
                  name: Integração Loja X
                  token: a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8
                  balance_sms: 0
                  status: active
                  activation_cost: 0
        "401":
          $ref: '#/components/responses/Unauthorized'
        "402":
          description: Saldo insuficiente para cobrir o custo de ativação.
          content:
            application/json:
              example:
                status: false
                message: "Saldo insuficiente. Criação custa 10 créditos."
                data: null

  /keys/{id}:
    get:
      summary: Obter chave de API
      tags: [Chaves de API]
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/keyId'
      responses:
        "200":
          description: Dados da chave (sem expor o token completo).
          content:
            application/json:
              example:
                status: true
                data:
                  id: 1
                  name: Integração Loja X
                  description: Envios da loja online
                  token_prefix: a1b2c3d4
                  balance_sms: 200
                  status: active
                  activation_cost: 0
                  created_at: "2025-06-01 09:00:00"
                  blocked_at: null
                  cancelled_at: null
        "401":
          $ref: '#/components/responses/Unauthorized'
        "404":
          $ref: '#/components/responses/NotFound'

    put:
      summary: Atualizar nome e descrição
      tags: [Chaves de API]
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/keyId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                  maxLength: 100
                  example: Loja X — Produção
                description:
                  type: string
                  nullable: true
                  example: Ambiente de produção
      responses:
        "200":
          description: Chave atualizada.
          content:
            application/json:
              example:
                status: true
                message: Chave atualizada.
                data: null
        "401":
          $ref: '#/components/responses/Unauthorized'
        "403":
          description: Chave cancelada não pode ser editada.
          content:
            application/json:
              example:
                status: false
                message: Chave cancelada não pode ser editada.
                data: null
        "404":
          $ref: '#/components/responses/NotFound'

    delete:
      summary: Cancelar chave de API
      description: Cancela permanentemente. Os créditos restantes ficam congelados.
      tags: [Chaves de API]
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/keyId'
      responses:
        "200":
          description: Chave cancelada.
          content:
            application/json:
              example:
                status: true
                message: Chave cancelada. Créditos restantes ficam congelados.
                data:
                  frozen_credits: 200
        "401":
          $ref: '#/components/responses/Unauthorized'
        "404":
          $ref: '#/components/responses/NotFound'
        "409":
          description: Chave já está cancelada.
          content:
            application/json:
              example:
                status: false
                message: Chave já cancelada.
                data: null

  /keys/{id}/credits:
    post:
      summary: Mover créditos para/de uma chave
      description: |
        Transfere créditos entre a conta principal e a chave atomicamente.
        - `action: "add"` — debita da conta principal e credita na chave.
        - `action: "remove"` — debita da chave e credita na conta principal.
      tags: [Chaves de API]
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/keyId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [action, amount]
              properties:
                action:
                  type: string
                  enum: [add, remove]
                  example: add
                amount:
                  type: integer
                  minimum: 1
                  example: 100
      responses:
        "200":
          description: Créditos movidos.
          content:
            application/json:
              example:
                status: true
                message: Créditos movidos com sucesso.
                data:
                  key_balance: 300
                  main_balance: 4699
        "401":
          $ref: '#/components/responses/Unauthorized'
        "403":
          description: Chave não está ativa.
          content:
            application/json:
              example:
                status: false
                message: Créditos só podem ser movidos em chaves ativas.
                data: null
        "404":
          $ref: '#/components/responses/NotFound'
        "422":
          description: Saldo insuficiente.
          content:
            application/json:
              example:
                status: false
                message: "Saldo insuficiente na conta principal (50 créditos)."
                data: null

  # ────────────── 2FA ───────────────────────────────────────────────────────
  /2fa/send:
    post:
      summary: Enviar código de verificação por SMS
      description: Gera e envia um código para o número informado. Retorna um id para verificação. Expira em 10 minutos.
      tags: [2FA]
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [destination_number]
              properties:
                destination_number:
                  type: string
                  example: "5511999999999"
                  description: Número celular brasileiro (55 + DDD + 9XXXXXXXX)
                message:
                  type: string
                  example: "Seu código é {code}. Válido por 10 minutos."
                  description: Texto customizado com {code}; quebra, tabulacao e \n, \r ou \t sao normalizados antes de gravar/enviar.
                chars_length:
                  type: integer
                  minimum: 4
                  maximum: 10
                  default: 6
                alpha_numeric:
                  type: boolean
                  default: false
                  description: true para código alfanumérico, false para apenas números
      responses:
        "201":
          description: Código enviado.
          content:
            application/json:
              example:
                status: true
                message: 2FA code sent successfully.
                data:
                  id: a3f8d2c1b9e7f4d5c6b7a8
                  expires_at: "2025-06-01 10:40:00"
                  destination: "5511999999999"
        "401":
          $ref: '#/components/responses/Unauthorized'
        "402":
          description: Saldo insuficiente.
          content:
            application/json:
              example:
                status: false
                message: User does not have sufficient SMS credits.
                data: null

  /2fa/verify:
    get:
      summary: Verificar código de 2FA
      tags: [2FA]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: query
          required: true
          schema:
            type: string
          description: ID retornado pelo /2fa/send
          example: a3f8d2c1b9e7f4d5c6b7a8
        - name: pin
          in: query
          required: true
          schema:
            type: string
          example: "482193"
      responses:
        "200":
          description: Código válido.
          content:
            application/json:
              example:
                status: true
                message: PIN verified successfully.
                data:
                  id: a3f8d2c1b9e7f4d5c6b7a8
                  verified: true
        "401":
          $ref: '#/components/responses/Unauthorized'
        "404":
          description: Código não encontrado.
          $ref: '#/components/responses/NotFound'
        "409":
          description: Código já utilizado.
          content:
            application/json:
              example:
                status: false
                message: 2FA code already used.
                data: null
        "410":
          description: Código expirado.
          content:
            application/json:
              example:
                status: false
                message: 2FA code has expired.
                data: null

  # ────────────── Suporte ─────────────────────────────────────────────────────
  /app/messages:
    get:
      summary: Listar mensagens do usuário com filtros e totais
      description: |
        Retorna mensagens do usuário autenticado com paginação, filtros e totais por status.

        O campo `replies_total` indica o número de respostas SMS recebidas no período filtrado.
      tags: [Painel]
      security:
        - bearerAuth: []
      parameters:
        - in: query
          name: page
          schema: { type: integer, default: 1 }
        - in: query
          name: per_page
          schema: { type: integer, default: 25, maximum: 100 }
        - in: query
          name: q
          description: Busca por número de telefone ou texto/tag da mensagem
          schema: { type: string }
        - in: query
          name: status
          schema: { type: string, enum: [queued, processed, sent, delivered, error, blocked, cancelled] }
        - in: query
          name: dt_ini
          schema: { type: string, format: date }
        - in: query
          name: dt_fim
          schema: { type: string, format: date }
      responses:
        "200":
          description: Lista de mensagens com totais por status e total de respostas recebidas.
          content:
            application/json:
              example:
                status: true
                message: Mensagens carregadas.
                data:
                  messages:
                    - message_id: a3f8d2c1b9e7f4d5
                      destination_number: "5511999999999"
                      message: Seu código é 4821.
                      tag: otp
                      status: delivered
                      created_at: "2026-06-01 10:30:00"
                      sent_at: "2026-06-01 10:30:05"
                      delivered_at: "2026-06-01 10:30:12"
                      error_at: null
                  total: 1
                  total_pages: 1
                  page: 1
                  status_totals:
                    queued: 0
                    processed: 0
                    sent: 0
                    delivered: 1
                    error: 0
                    blocked: 0
                    cancelled: 0
                  replies_total: 1
        "401":
          $ref: '#/components/responses/Unauthorized'

  /app/messages/export:
    post:
      summary: Exporta mensagens do usuário em CSV
      description: |
        Gera um CSV (streaming, máx 50k linhas) das mensagens do usuário autenticado,
        aplicando os mesmos filtros da listagem. Operadora nunca é exposta; status internos
        (`processing`/`invalid`) são mapeados para os rótulos canônicos.
      tags: [Painel]
      security:
        - bearerAuth: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                filters:
                  type: object
                  properties:
                    q:      { type: string }
                    status: { type: string, enum: [queued, processed, sent, delivered, error, blocked, cancelled] }
                    tag:    { type: string }
                    dt_ini: { type: string, format: date }
                    dt_fim: { type: string, format: date }
      responses:
        "200":
          description: Arquivo CSV (text/csv) com as mensagens filtradas.
          content:
            text/csv:
              schema: { type: string, format: binary }
        "401":
          $ref: '#/components/responses/Unauthorized'

  /app/extrato:
    get:
      summary: Extrato de créditos do usuário
      description: |
        Retorna o histórico de movimentação de créditos do usuário com filtros, paginação
        e resumo do período.

        **Parâmetro `q`** — busca inteligente:
        - Somente dígitos com ≥ 8 caracteres → busca por número de destino do SMS
        - Qualquer outro valor → busca por `transaction_id`, `message_id` ou `origin_message_id`

        **Campo `origin_message_id`**: presente nas entradas de SMS recebido (`description` = "SMS recebido de +…").
        Contém o `message_id` da mensagem enviada originalmente que gerou a resposta.
        O `message_id` da entrada de SMS recebido corresponde ao `reply_id` da resposta.
      tags: [Painel]
      security:
        - bearerAuth: []
      parameters:
        - in: query
          name: q
          description: "Busca inteligente: dígitos ≥8 chars → número de destino; demais → transaction_id / message_id / origin_message_id"
          schema: { type: string }
        - in: query
          name: type
          description: Tipo de lançamento
          schema: { type: string, enum: [purchase, usage, transfer, adjustment] }
        - in: query
          name: movement
          description: Direção do movimento
          schema: { type: string, enum: [credit, debit] }
        - in: query
          name: dt_ini
          schema: { type: string, format: date }
        - in: query
          name: dt_fim
          schema: { type: string, format: date }
        - in: query
          name: page
          schema: { type: integer, default: 1 }
        - in: query
          name: per_page
          schema: { type: integer, default: 25, maximum: 100 }
      responses:
        "200":
          description: Histórico de créditos com resumo do período.
          content:
            application/json:
              example:
                status: true
                message: Extrato carregado.
                data:
                  entries:
                    - transaction_id: abc123def456
                      statement_type: usage
                      movement_type: debit
                      previous_credits: 1001
                      credits_amount: 1
                      new_credits: 1000
                      description: SMS enviado
                      message_id: a3f8d2c1b9e7f4d5
                      origin_message_id: null
                      created_at: "2026-06-01 10:30:00"
                    - transaction_id: xyz789abc123
                      statement_type: usage
                      movement_type: debit
                      previous_credits: 1000
                      credits_amount: 1
                      new_credits: 999
                      description: "SMS recebido de +5511988887777"
                      message_id: reply_b7e3d1c9a2f8
                      origin_message_id: a3f8d2c1b9e7f4d5
                      created_at: "2026-06-01 10:31:00"
                  total: 2
                  total_pages: 1
                  page: 1
                  summary:
                    total_credit: 0
                    total_debit: 2
                    balance_change: -2
        "401":
          $ref: '#/components/responses/Unauthorized'

  # ────────────── Admin ───────────────────────────────────────────────────────

x-webhooks:
  sms-status-update:
    post:
      summary: Notificação de status do SMS
      description: |
        Payload enviado via POST para a `webhook_url` informada no envio quando o status
        de uma mensagem muda. Configure sua URL para receber e processar estes eventos.

        A plataforma aguarda resposta HTTP 2xx em até 10 segundos. Em caso de falha, há
        até 5 tentativas com backoff exponencial (30s → 5min → 30min → 2h → 10h).
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                version:
                  type: string
                  example: v3
                id:
                  type: string
                  example: a3f8d2c1b9e7f4d5
                status:
                  type: string
                  enum: [queued, processed, sent, delivered, error, blocked, cancelled]
                  example: delivered
                destination_number:
                  type: string
                  example: "5511999999999"
                created_at:
                  type: string
                  format: date-time
                sent_at:
                  type: string
                  format: date-time
                  nullable: true
                delivered_at:
                  type: string
                  format: date-time
                  nullable: true
                error_at:
                  type: string
                  format: date-time
                  nullable: true
                blocked_at:
                  type: string
                  format: date-time
                  nullable: true
            example:
              version: v3
              id: a3f8d2c1b9e7f4d5
              status: delivered
              destination_number: "5511999999999"
              created_at: "2025-06-01 10:30:00"
              sent_at: "2025-06-01 10:30:05"
              delivered_at: "2025-06-01 10:30:12"
              error_at: null
              blocked_at: null
      responses:
        "2XX":
          description: Confirmação recebida. Qualquer status 2xx é aceito.

  sms-reply:
    post:
      summary: Resposta SMS recebida
      description: |
        Payload enviado via POST para a `webhook_url` da mensagem original quando o
        destinatário responde ao SMS. Disponível apenas para operadoras com suporte a
        respostas (ApiBrasil, Witi, Notreve SMS).

        O campo `original_id` identifica a mensagem enviada originalmente.
        A plataforma aguarda resposta HTTP 2xx em até 10 segundos. Em caso de falha,
        há até 5 tentativas com backoff exponencial (30s → 5min → 30min → 2h → 10h).

        > **Pré-requisito**: o envio deve ter `message_reply: "yes"` para que a
        > operadora associe a resposta à mensagem original.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                version:
                  type: string
                  example: v3
                type:
                  type: string
                  enum: [sms_reply]
                  example: sms_reply
                id:
                  type: string
                  description: Identificador único da resposta gerado pela plataforma.
                  example: b7c3e4d2a1f9g5h6
                original_id:
                  type: string
                  description: ID da mensagem enviada originalmente.
                  example: a3f8d2c1b9e7f4d5
                from_number:
                  type: string
                  description: Número que enviou a resposta, formato +DDI DDD número.
                  example: "+5511999999999"
                text:
                  type: string
                  description: Texto da resposta recebida.
                  example: "Confirmado"
                received_at:
                  type: string
                  format: date-time
                  example: "2025-06-01 10:35:00"
            example:
              version: v3
              type: sms_reply
              id: b7c3e4d2a1f9g5h6
              original_id: a3f8d2c1b9e7f4d5
              from_number: "+5511999999999"
              text: "Confirmado"
              received_at: "2025-06-01 10:35:00"
      responses:
        "2XX":
          description: Confirmação recebida. Qualquer status 2xx é aceito.