openapi: 3.1.0
info:
  title: Inboxa API
  version: 0.1.0
  description: |
    API da Inboxa — caixas de entrada de email para agentes de IA.

    **Conceitos:**
    - **Inbox**: uma caixa de email real (`vendas@inboxa.email`) criada e operada via API.
    - **Thread**: conversa agrupada automaticamente pelos headers RFC 5322.
    - **Message**: email enviado ou recebido dentro de uma thread.
    - **Webhook**: notificação HTTP assinada (HMAC-SHA256) para eventos de email.

    **Autenticação**: header `Authorization: Bearer ibx_...`. Chaves podem ter
    escopo de organização (acessam tudo) ou de inbox (acessam uma única caixa).

    **Sandbox**: organizações não verificadas operam em modo sandbox — emails
    circulam apenas entre caixas internas da Inboxa. A verificação de CNPJ/CPF
    libera envio externo.
  contact:
    name: Inboxa
    url: https://inboxa.com.br
servers:
  - url: https://api.inboxa.com.br/v0
    description: Produção
tags:
  - name: Inboxes
    description: Criação e gestão de caixas
  - name: Threads
    description: Conversas de uma caixa
  - name: Messages
    description: Envio, resposta, encaminhamento e leitura
  - name: Attachments
    description: Download de anexos
  - name: Webhooks
    description: Notificações de eventos
  - name: API Keys
    description: Chaves de acesso

security:
  - bearerAuth: []

paths:
  /inboxes:
    post:
      tags: [Inboxes]
      operationId: createInbox
      summary: Criar caixa
      description: Cria uma caixa no domínio de fábrica (`inboxa.email`) ou em um domínio customizado verificado da organização.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [username]
              properties:
                username:
                  type: string
                  pattern: '^[a-z0-9][a-z0-9._-]{1,62}$'
                  description: Parte local do endereço (antes do @).
                  examples: [cobranca]
                domain:
                  type: string
                  description: Domínio da caixa. Padrão `inboxa.email`. Domínios customizados precisam estar verificados.
                  default: inboxa.email
                display_name:
                  type: string
                  maxLength: 100
                  examples: [Agente de Cobrança]
                pod_id:
                  type: string
                  description: Pod (agrupamento multi-tenant) ao qual a caixa pertence.
      responses:
        '201':
          description: Caixa criada.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Inbox' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '409':
          description: Endereço já existe.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
    get:
      tags: [Inboxes]
      operationId: listInboxes
      summary: Listar caixas
      parameters:
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/cursor'
        - name: pod_id
          in: query
          schema: { type: string }
      responses:
        '200':
          description: Lista paginada.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Inbox' }
                  next_cursor:
                    type: [string, 'null']

  /inboxes/{inbox_id}:
    parameters:
      - $ref: '#/components/parameters/inbox_id'
    get:
      tags: [Inboxes]
      operationId: getInbox
      summary: Obter caixa
      responses:
        '200':
          description: Caixa.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Inbox' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Inboxes]
      operationId: deleteInbox
      summary: Excluir caixa
      description: Exclui a caixa e agenda a exclusão do conteúdo conforme a política de retenção da organização.
      responses:
        '204': { description: Excluída. }
        '404': { $ref: '#/components/responses/NotFound' }

  /inboxes/{inbox_id}/threads:
    parameters:
      - $ref: '#/components/parameters/inbox_id'
    get:
      tags: [Threads]
      operationId: listThreads
      summary: Listar threads
      parameters:
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/cursor'
        - name: q
          in: query
          description: Busca full-text em assunto e corpo.
          schema: { type: string }
      responses:
        '200':
          description: Threads da caixa, mais recentes primeiro.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Thread' }
                  next_cursor:
                    type: [string, 'null']
        '404': { $ref: '#/components/responses/NotFound' }

  /inboxes/{inbox_id}/threads/{thread_id}:
    parameters:
      - $ref: '#/components/parameters/inbox_id'
      - name: thread_id
        in: path
        required: true
        schema: { type: string }
    get:
      tags: [Threads]
      operationId: getThread
      summary: Obter thread com mensagens
      responses:
        '200':
          description: Thread completa.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ThreadWithMessages' }
        '404': { $ref: '#/components/responses/NotFound' }

  /inboxes/{inbox_id}/messages/send:
    parameters:
      - $ref: '#/components/parameters/inbox_id'
    post:
      tags: [Messages]
      operationId: sendMessage
      summary: Enviar mensagem
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/SendMessageRequest' }
      responses:
        '202':
          description: Mensagem aceita para envio.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Message' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '403':
          description: Envio rejeitado (sandbox, limite do plano ou destinatário bloqueado).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /inboxes/{inbox_id}/messages/{message_id}:
    parameters:
      - $ref: '#/components/parameters/inbox_id'
      - $ref: '#/components/parameters/message_id'
    get:
      tags: [Messages]
      operationId: getMessage
      summary: Obter mensagem
      responses:
        '200':
          description: Mensagem.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Message' }
        '404': { $ref: '#/components/responses/NotFound' }

  /inboxes/{inbox_id}/messages/{message_id}/reply:
    parameters:
      - $ref: '#/components/parameters/inbox_id'
      - $ref: '#/components/parameters/message_id'
    post:
      tags: [Messages]
      operationId: replyToMessage
      summary: Responder na thread
      description: Responde ao remetente mantendo os headers de threading (`In-Reply-To`, `References`).
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ReplyRequest' }
      responses:
        '202':
          description: Resposta aceita para envio.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Message' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }

  /inboxes/{inbox_id}/messages/{message_id}/forward:
    parameters:
      - $ref: '#/components/parameters/inbox_id'
      - $ref: '#/components/parameters/message_id'
    post:
      tags: [Messages]
      operationId: forwardMessage
      summary: Encaminhar mensagem
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [to]
              properties:
                to:
                  type: array
                  items: { type: string, format: email }
                text: { type: string }
                html: { type: string }
      responses:
        '202':
          description: Encaminhamento aceito.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Message' }
        '404': { $ref: '#/components/responses/NotFound' }

  /inboxes/{inbox_id}/attachments/{attachment_id}:
    parameters:
      - $ref: '#/components/parameters/inbox_id'
      - name: attachment_id
        in: path
        required: true
        schema: { type: string }
    get:
      tags: [Attachments]
      operationId: downloadAttachment
      summary: Baixar anexo
      responses:
        '302':
          description: Redireciona para URL assinada de download (expira em 15 minutos).
        '404': { $ref: '#/components/responses/NotFound' }

  /webhooks:
    post:
      tags: [Webhooks]
      operationId: createWebhook
      summary: Criar webhook
      description: |
        Cada entrega inclui os headers `X-Inboxa-Signature` (HMAC-SHA256 do corpo,
        com o `secret` retornado na criação) e `X-Inboxa-Event`. Entregas com
        falha são repetidas com backoff exponencial por até 24h.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, events]
              properties:
                url: { type: string, format: uri }
                events:
                  type: array
                  items:
                    type: string
                    enum: [message.received, message.delivered, message.bounced, message.complained]
                inbox_ids:
                  type: array
                  description: Restringe aos eventos dessas caixas. Vazio = todas.
                  items: { type: string }
      responses:
        '201':
          description: Webhook criado. O `secret` só é retornado nesta resposta.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WebhookWithSecret' }
        '400': { $ref: '#/components/responses/BadRequest' }
    get:
      tags: [Webhooks]
      operationId: listWebhooks
      summary: Listar webhooks
      responses:
        '200':
          description: Webhooks da organização.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Webhook' }

  /webhooks/{webhook_id}:
    parameters:
      - name: webhook_id
        in: path
        required: true
        schema: { type: string }
    delete:
      tags: [Webhooks]
      operationId: deleteWebhook
      summary: Excluir webhook
      responses:
        '204': { description: Excluído. }
        '404': { $ref: '#/components/responses/NotFound' }

  /api-keys:
    post:
      tags: [API Keys]
      operationId: createApiKey
      summary: Criar chave de API
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, scope]
              properties:
                name: { type: string, maxLength: 100 }
                scope:
                  type: string
                  enum: [org, inbox]
                inbox_id:
                  type: string
                  description: Obrigatório quando `scope = inbox`.
      responses:
        '201':
          description: Chave criada. O valor (`ibx_...`) só é retornado nesta resposta.
          content:
            application/json:
              schema:
                type: object
                properties:
                  api_key_id: { type: string }
                  name: { type: string }
                  scope: { type: string, enum: [org, inbox] }
                  inbox_id: { type: [string, 'null'] }
                  key:
                    type: string
                    description: Valor completo da chave — armazene com segurança.

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: 'ibx_<random>'

  parameters:
    inbox_id:
      name: inbox_id
      in: path
      required: true
      schema: { type: string }
    message_id:
      name: message_id
      in: path
      required: true
      schema: { type: string }
    limit:
      name: limit
      in: query
      schema: { type: integer, minimum: 1, maximum: 100, default: 25 }
    cursor:
      name: cursor
      in: query
      schema: { type: string }

  responses:
    BadRequest:
      description: Requisição inválida.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    NotFound:
      description: Recurso não encontrado.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }

  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              examples: [inbox_not_found, sandbox_restricted, rate_limited]
            message: { type: string }

    Inbox:
      type: object
      required: [inbox_id, address, created_at]
      properties:
        inbox_id: { type: string, examples: [ibx_in_9f8a7b] }
        address: { type: string, format: email, examples: [cobranca@inboxa.email] }
        display_name: { type: [string, 'null'] }
        pod_id: { type: [string, 'null'] }
        sandbox:
          type: boolean
          description: Caixa restrita ao modo sandbox.
        created_at: { type: string, format: date-time }

    Thread:
      type: object
      required: [thread_id, subject, last_message_at, message_count]
      properties:
        thread_id: { type: string }
        subject: { type: [string, 'null'] }
        participants:
          type: array
          items: { type: string, format: email }
        message_count: { type: integer }
        last_message_at: { type: string, format: date-time }

    ThreadWithMessages:
      allOf:
        - $ref: '#/components/schemas/Thread'
        - type: object
          properties:
            messages:
              type: array
              items: { $ref: '#/components/schemas/Message' }

    Message:
      type: object
      required: [message_id, thread_id, direction, from, to, created_at]
      properties:
        message_id: { type: string }
        thread_id: { type: string }
        direction: { type: string, enum: [inbound, outbound] }
        from: { type: string, format: email }
        to:
          type: array
          items: { type: string, format: email }
        cc:
          type: array
          items: { type: string, format: email }
        subject: { type: [string, 'null'] }
        text: { type: [string, 'null'] }
        html: { type: [string, 'null'] }
        labels:
          type: array
          items: { type: string }
        attachments:
          type: array
          items: { $ref: '#/components/schemas/AttachmentMeta' }
        delivery_status:
          type: [string, 'null']
          enum: [queued, sent, delivered, bounced, complained, null]
          description: Apenas para mensagens outbound.
        created_at: { type: string, format: date-time }

    AttachmentMeta:
      type: object
      required: [attachment_id, filename, content_type, size]
      properties:
        attachment_id: { type: string }
        filename: { type: string }
        content_type: { type: string }
        size: { type: integer, description: Tamanho em bytes. }

    SendMessageRequest:
      type: object
      required: [to, subject]
      properties:
        to:
          type: array
          minItems: 1
          items: { type: string, format: email }
        cc:
          type: array
          items: { type: string, format: email }
        bcc:
          type: array
          items: { type: string, format: email }
        subject: { type: string, maxLength: 998 }
        text: { type: string }
        html: { type: string }
        labels:
          type: array
          items: { type: string }
        attachments:
          type: array
          items:
            type: object
            required: [filename, content_type, content_base64]
            properties:
              filename: { type: string }
              content_type: { type: string }
              content_base64: { type: string }

    ReplyRequest:
      type: object
      properties:
        text: { type: string }
        html: { type: string }
        cc:
          type: array
          items: { type: string, format: email }
        attachments:
          type: array
          items:
            type: object
            required: [filename, content_type, content_base64]
            properties:
              filename: { type: string }
              content_type: { type: string }
              content_base64: { type: string }

    Webhook:
      type: object
      required: [webhook_id, url, events]
      properties:
        webhook_id: { type: string }
        url: { type: string, format: uri }
        events:
          type: array
          items: { type: string }
        inbox_ids:
          type: array
          items: { type: string }
        enabled: { type: boolean, default: true }

    WebhookWithSecret:
      allOf:
        - $ref: '#/components/schemas/Webhook'
        - type: object
          required: [secret]
          properties:
            secret:
              type: string
              description: Usado para validar `X-Inboxa-Signature`. Só retornado na criação.

webhooks:
  message.received:
    post:
      summary: Novo email recebido em uma caixa
      description: Enviado ao endpoint configurado quando uma caixa recebe um email.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                event: { type: string, const: message.received }
                inbox_id: { type: string }
                thread_id: { type: string }
                message: { $ref: '#/components/schemas/Message' }
      responses:
        '200':
          description: Confirme com 2xx em até 10s; caso contrário a entrega será repetida.
