openapi: 3.0.3
info:
  title: 'Br Envio API v1'
  description: |
    # Bem-vindo à API do Br Envio

    Esta documentação é para **desenvolvedores** que integram o Br Envio a outro sistema (CRM, loja, ERP, site próprio ou legado).

    Você **não** precisa conhecer o painel por dentro. O fluxo é:

    1. Criar um **token de API** no painel (uma vez).
    2. Chamar a API com `Authorization: Bearer …`.
    3. Ler a resposta no formato padrão (`success` / `error`).

    Se você é júnior: leia esta página de cima a baixo **uma vez**, depois use a referência de endpoints à esquerda (ou abaixo) como dicionário.

    ---

    ## O que esta API faz (e o que não faz)

    ### Faz (publicado aqui)

    | Capacidade | Para quê | Endpoint principal |
    |------------|----------|--------------------|
    | **Upsert de contato** | Criar ou atualizar um contato pelo e-mail (sync CRM) | `POST /api/v1/integration/contacts` |
    | **Tags** | Listar, criar, renomear e excluir marcadores | `/api/v1/integration/tags` |
    | **Envio avulso** | Enviar **um** e-mail por chamada (recibo, senha, aviso 1:1) | `POST /api/v1/messages` |
    | **Webhooks (saída)** | Receber eventos na **sua** URL (HMAC) | Documentado abaixo — cadastro no painel |

    ### Não faz (use o painel)

    Campanhas em massa, editor visual, setup de domínio/DNS, faturamento, Super Admin, kit de marca, pixel do site, segmentos/forms e gestão de tokens **não** têm endpoint público com token de API neste portal. Isso é intencional: o integrador sincroniza audiência e envia 1:1; o marketing em massa e a marca ficam no [painel](https://painel.brenvio.com.br/).

    **Hub VTEX** (sync de clientes e carrinho abandonado) também é tela do painel + webhook da loja — não é rota Bearer deste portal.

    Guia leigo (não-dev): [Centro de ajuda](https://ajuda.brenvio.com.br/).

    Páginas de produto (marketing / SEO): [Segmentos](https://novo.brenvio.com.br/segmentos/), [Formulários](https://novo.brenvio.com.br/formularios/), [Kit de marca](https://novo.brenvio.com.br/kit-de-marca/), [Pixel](https://novo.brenvio.com.br/pixel-do-site/), [VTEX](https://novo.brenvio.com.br/integracao-vtex/), [API e webhooks](https://novo.brenvio.com.br/api-e-webhooks/).

    ---

    ## Comece em 5 minutos

    ### Passo 1 — Crie o token

    1. Entre em [painel.brenvio.com.br](https://painel.brenvio.com.br/).
    2. Vá em **Integrações → Tokens de API**.
    3. Crie um token, dê um nome (ex.: `CRM produção`) e **copie o valor na hora**.
    4. Guarde em variável de ambiente ou cofre de secrets — **não** commite no Git.

    O valor completo só aparece **uma vez**. Se perder, revogue e crie outro.

    Guia com prints: [Tokens de API](https://ajuda.brenvio.com.br/pt-BR/getting-started/api-tokens).

    ### Passo 2 — Faça a primeira chamada

    Base URL de produção:

    ```text
    https://api.brenvio.com.br
    ```

    Exemplo com **curl** (criar/atualizar um contato):

    ```bash
    curl --request POST \
      "https://api.brenvio.com.br/api/v1/integration/contacts" \
      --header "Authorization: Bearer SEU_TOKEN_AQUI" \
      --header "Content-Type: application/json" \
      --header "Accept: application/json" \
      --data "{
        \"email\": \"helena@empresa.test\",
        \"name\": \"Helena\",
        \"tags\": [\"Lead CRM\"]
      }"
    ```

    O mesmo em **PHP** (cURL nativo — sem Composer):

    ```php
    <?php
    $token = getenv('BRENVIO_API_TOKEN') ?: 'SEU_TOKEN_AQUI';
    $payload = json_encode([
        'email' => 'helena@empresa.test',
        'name' => 'Helena',
        'tags' => ['Lead CRM'],
    ], JSON_UNESCAPED_UNICODE);

    $ch = curl_init('https://api.brenvio.com.br/api/v1/integration/contacts');
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_HTTPHEADER => [
            'Authorization: Bearer '.$token,
            'Content-Type: application/json',
            'Accept: application/json',
        ],
        CURLOPT_POSTFIELDS => $payload,
        CURLOPT_RETURNTRANSFER => true,
    ]);
    $body = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    $data = json_decode((string) $body, true);
    if ($status >= 200 && $status < 300 && ($data['success'] ?? false)) {
        echo 'OK — created='.(($data['data']['created'] ?? false) ? 'true' : 'false').PHP_EOL;
    } else {
        fwrite(STDERR, 'Erro HTTP '.$status.': '.($data['error']['code'] ?? $body).PHP_EOL);
    }
    ```

    O mesmo em **Node.js** (fetch nativo, Node 18+):

    ```javascript
    const token = process.env.BRENVIO_API_TOKEN || 'SEU_TOKEN_AQUI';

    const res = await fetch('https://api.brenvio.com.br/api/v1/integration/contacts', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${token}`,
        'Content-Type': 'application/json',
        Accept: 'application/json',
      },
      body: JSON.stringify({
        email: 'helena@empresa.test',
        name: 'Helena',
        tags: ['Lead CRM'],
      }),
    });

    const data = await res.json();
    if (res.ok && data.success) {
      console.log('OK — created=', data.data?.created);
    } else {
      console.error('Erro', res.status, data.error?.code || data);
    }
    ```

    > Ainda **não** publicamos pacotes npm/Composer oficiais — use estes snippets (ou gere cliente a partir do [OpenAPI](openapi.yaml)).

    Se der certo, você recebe HTTP **200** (atualizado) ou **201** (criado) com JSON assim:

    ```json
    {
      "success": true,
      "message": "Contato criado via integração.",
      "data": {
        "id": 1,
        "email": "helena@empresa.test",
        "created": true
      }
    }
    ```

    ### Passo 3 — Entenda sucesso vs erro

    **Sempre** olhe o campo `success` (e o código HTTP).

    Sucesso:

    ```json
    { "success": true, "data": { }, "message": "texto opcional" }
    ```

    Erro:

    ```json
    {
      "success": false,
      "error": {
        "code": "CODIGO_ESTAVEL",
        "message": "Frase legível para humanos"
      }
    }
    ```

    - Use `error.code` no seu `if` / `switch` (não parseie a mensagem).
    - Use `error.message` para log ou tela de suporte.
    - Idioma da mensagem: cabeçalho opcional `Accept-Language: pt-BR` (também `en` / `es`).

    ### Passo 4 — (Opcional) Envie um e-mail de teste 1:1

    Só funciona se o **setup de envio** (domínio/remetente) da conta estiver completo no painel.

    ```bash
    curl --request POST \
      "https://api.brenvio.com.br/api/v1/messages" \
      --header "Authorization: Bearer SEU_TOKEN_AQUI" \
      --header "Content-Type: application/json" \
      --header "Accept: application/json" \
      --data "{
        \"to_email\": \"voce@empresa.test\",
        \"subject\": \"Teste API Br Envio\",
        \"text_body\": \"Olá! Este é um teste.\",
        \"message_type\": \"transactional\"
      }"
    ```

    Resposta **202** = aceito na fila (ainda não significa “já chegou na caixa”). Consulte `GET /api/v1/messages/{id}` ou configure um webhook.

    ---

    ## Autenticação (detalhe)

    | Item | Valor |
    |------|--------|
    | Tipo | **API token** (não é o login JWT do painel) |
    | Cabeçalho | `Authorization: Bearer <token>` |
    | Rate limit | **60 requisições por minuto** por token |
    | Escopo | Sempre a **conta** dona do token (não há “trocar de cliente” na API) |

    Erros comuns:

    | HTTP | `error.code` | O que fazer |
    |------|--------------|-------------|
    | 401 | `UNAUTHORIZED` | Token ausente, inválido ou revogado |
    | 429 | `RATE_LIMIT_EXCEEDED` | Espere e tente de novo; reduza o ritmo |

    ---

    ## Formato das respostas (envelope)

    Todas as rotas deste portal usam o mesmo envelope JSON.

    **Lista paginada** (ex.: tags) também traz `meta` com `current_page`, `per_page`, `total`, `last_page`.

    Códigos de erro que você vai ver com frequência:

    | Código | Significado simples |
    |--------|---------------------|
    | `UNAUTHORIZED` | Sem token válido |
    | `VALIDATION_ERROR` | Body/query inválidos (campos faltando ou tipo errado) |
    | `CONTACT_BLOCKLISTED` | E-mail/domínio na lista de bloqueio da conta |
    | `CONTACT_SUPPRESSED` | E-mail/domínio na supressão global (bounce/reclamação grave) |
    | `UNKNOWN_FIELD_SLUG` | Slug em `fields` não existe no catálogo da conta |
    | `QUOTA_EXCEEDED` | Cota de e-mails do plano acabou neste ciclo |
    | `SETUP_INCOMPLETE` | Domínio/remetente ainda não configurados |
    | `MESSAGE_TYPE_REQUIRED` | Faltou `message_type` no envio avulso |
    | `RATE_LIMIT_EXCEEDED` | Mais de 60 req/min neste token |
    | `NOT_FOUND` | ID inexistente **ou** de outra conta (não revelamos cross-tenant) |

    ---

    ## Receita A — Sincronizar contatos do CRM

    Objetivo: o CRM manda a verdade do contato (e-mail + nome + tags).

    1. Para cada contato alterado no CRM, chame `POST /api/v1/integration/contacts`.
    2. Chave de identidade = **e-mail** (sempre normalizado em minúsculas no servidor).
    3. Tags:
       - `tag_match` omitido ou `"append"` → **anexa** tags sem remover as que já existem.
       - `"replace"` → o array enviado vira o **conjunto completo** (CRM manda a verdade).
    4. Campos tipados: objeto `fields` com slugs cadastrados em **Audiência → Campos da base**. Upsert **parcial** (só os slugs enviados mudam).
    5. Telefone: preferir `country_code` + `phone_number` (só dígitos). Alternativa: string `phone` (o servidor normaliza; DDI padrão `55` se faltar).

    **Higiene (não negociável):**

    - Lista de bloqueio da conta → rejeitado (`CONTACT_BLOCKLISTED`).
    - Supressão global → rejeitado (`CONTACT_SUPPRESSED`).
    - Contato já `unsubscribed` ou `suppressed` **não** é reativado pelo upsert.

    Resposta: `data.created = true` com HTTP 201 (novo) ou `false` com HTTP 200 (atualizado).

    ---

    ## Receita B — Enviar recibo / senha (transacional)

    Objetivo: seu sistema dispara **um** e-mail por evento de negócio.

    1. `message_type`: `"transactional"` (recibo, senha, aviso operacional).
    2. Envie `html_body` **ou** `text_body` (pelo menos um).
    3. Resposta **202** + `data.id` + `status: "pending"`.
    4. O envio real é **assíncrono** (fila). Não espere o provedor na mesma request HTTP.
    5. Consulte `GET /api/v1/messages/{id}` até `sent` / `failed` / `skipped`, **ou** assine webhooks `message.sent` / `message.failed`.

    Diferença importante:

    | Tipo | Uso | Descadastro de marketing |
    |------|-----|---------------------------|
    | `transactional` | Recibo, senha, NF | Não aplica unsub de marketing; ainda respeita blocklist/supressão e saúde da conta |
    | `marketing` | Promoção 1:1 | Respeita descadastro; use só se fizer sentido |

    **1 aceite (202) = 1 e-mail da cota** do plano.

    ---

    ## Receita C — Receber eventos (webhooks)

    Você cadastra a URL HTTPS no painel (**Integrações → Webhooks**). O CRUD **não** é endpoint deste portal; aqui documentamos o **contrato de entrega**.

    ### Cabeçalhos que enviamos

    | Header | Conteúdo |
    |--------|----------|
    | `Content-Type` | `application/json` |
    | `X-Brenvio-Signature` | HMAC-SHA256 do **corpo cru** (raw body) com o secret do endpoint (hex) |
    | `X-Brenvio-Event` | Nome do evento (ex.: `message.sent`) |
    | `Idempotency-Key` | ID estável da entrega — use para ignorar retries duplicados |

    ### Como validar a assinatura (obrigatório em produção)

    1. Leia o body **exatamente** como chegou (bytes/string, **sem** reformatar o JSON).
    2. Calcule HMAC-SHA256 com o `secret` mostrado **uma vez** ao criar/rotacionar o webhook.
    3. Compare com `X-Brenvio-Signature` em tempo constante.

    Exemplo PHP:

    ```php
    $raw = file_get_contents('php://input');
    $expected = hash_hmac('sha256', $raw, $secret);
    if (! hash_equals($expected, $_SERVER['HTTP_X_BRENVIO_SIGNATURE'] ?? '')) {
        http_response_code(401);
        exit;
    }
    ```

    Responda **2xx** rápido para marcarmos como entregue. Falhas geram novas tentativas com espera crescente.

    ### Eventos disponíveis

    | Evento | Quando |
    |--------|--------|
    | `contact.created` | Contato criado (painel / formulário / integração) |
    | `contact.updated` | Atualização relevante do contato |
    | `message.sent` | `POST /messages` concluído com sucesso |
    | `message.failed` | `POST /messages` falhou no provedor |
    | `campaign.send.completed` | Disparo de campanha em massa terminou |
    | `webhook.test` | Botão **Testar** no painel |

    ### Exemplo de payload (`message.sent`)

    ```json
    {
      "event": "message.sent",
      "delivery_id": "42",
      "occurred_at": "2026-07-30T15:00:00+00:00",
      "data": {
        "id": 10,
        "message_type": "transactional",
        "to_email": "cliente@empresa.test",
        "subject": "Seu pedido #123",
        "status": "sent",
        "sent_at": "2026-07-30T15:00:01+00:00"
      }
    }
    ```

    ---

    ## Onde fazer cada coisa

    | Capacidade | Onde |
    |------------|------|
    | Upsert contato / tags / envio 1:1 | **Esta API** |
    | Cadastrar URL de webhook / secret | Painel → Integrações → Webhooks |
    | Criar / revogar token | Painel → Integrações → Tokens de API |
    | Campanha em massa / editor / kit de marca | Painel → Campanhas / Conta |
    | Pixel do site (opt-in) | Painel → Integrações → Pixel |
    | VTEX sync + carrinho | Painel → Integrações / Automação |
    | Setup de domínio / DNS | Painel → Setup de envio |
    | Import CSV / segmentos / forms | Painel → Audiência / Automação |
    | Métricas | Painel → Envios |
    | Explicação leiga | [ajuda.brenvio.com.br](https://ajuda.brenvio.com.br/) |

    ---

    ## Como usar este portal

    1. Leia o **Getting started** (esta página).
    2. Na referência à esquerda, abra o endpoint e use **Try it** com o seu token (só em ambiente seguro).
    3. Baixe a [coleção Postman](collection.json) ou o [OpenAPI](openapi.yaml) se preferir gerar cliente.
    4. Em dúvida de produto (não de HTTP): abra o [Centro de ajuda](https://ajuda.brenvio.com.br/).

    Pronto — a partir daqui, cada endpoint explica parâmetros, exemplos e respostas com o máximo de detalhe possível.
    )
  version: 1.0.0
  contact:
    name: 'Br Envio'
    url: 'https://brenvio.com.br'
  x-logo:
    url: ./logo.png
    altText: 'Br Envio'
    href: 'https://docs.brenvio.com.br/'
servers:
  - url: 'https://api.brenvio.com.br'
tags:
  - name: 'Integração de Contatos'
    description: |-

      Sincronize contatos a partir do seu CRM, loja ou ERP.

      **Autenticação:** `Authorization: Bearer <api_token>` (token em Integrações → Tokens de API).
      **Limite:** 60 requisições/minuto por token.
      **Resposta:** envelope `{ success, data }` ou `{ success: false, error }`.

      A chave de identidade é sempre o **e-mail** (minúsculas). Tags e campos tipados
      são opcionais. Contatos descadastrados ou suprimidos **não** são reativados por esta rota.
  - name: Tags
    description: |-

      Catálogo de marcadores (tags) da conta — rótulos como "VIP", "Lead CRM", "Carrinho".

      **Neste portal** use apenas as rotas `/api/v1/integration/tags*` com
      `Authorization: Bearer <api_token>`. Limite: 60 req/min. Envelope `{ success, data }`.

      Para **vincular** tags a um contato, use o upsert de contatos (`tags` + `tag_match`),
      não estas rotas de catálogo.
  - name: 'Envio Avulso'
    description: |-

      Envie **um** e-mail por requisição (recibo, senha, aviso 1:1) — sem disparar campanha em massa.

      **Autenticação:** `Authorization: Bearer <api_token>`.
      **Limite:** 60 req/min. **Cota:** cada aceite (HTTP 202) consome **1** e-mail do plano.
      **Assíncrono:** 202 significa “entrou na fila”; o envio real ocorre em background.

      Pré-requisitos da conta: setup de domínio/remetente completo; cota disponível;
      conta sem bloqueio de saúde (circuito/congelamento).
components:
  securitySchemes:
    default:
      type: http
      scheme: bearer
      description: 'Cole o token completo no campo Bearer (ex.: comece com `be_…`). Crie em Integrações → Tokens de API: https://painel.brenvio.com.br/settings/api-tokens (o valor em texto só aparece na criação). Try-it do portal usa token **sandbox** (`be_sandbox_…`) sem SES real. Cabeçalho: `Authorization: Bearer COLE_SEU_TOKEN_AQUI`.'
security:
  -
    default: []
paths:
  /api/v1/integration/contacts:
    post:
      summary: 'Criar ou atualizar contato (upsert)'
      operationId: criarOuAtualizarContatoupsert
      description: |-
        Cria o contato se o e-mail for novo; atualiza se já existir na conta.

        **Tags:** envie nomes em `tags`. Nomes inexistentes são criados automaticamente.
        Com `tag_match=append` (padrão) as tags são **anexadas**. Com `replace`, o array
        enviado vira o conjunto completo (útil quando o CRM é a fonte da verdade).

        **Campos tipados (`fields`):** objeto `{ "slug": valor }` com slugs cadastrados em
        Audiência → Campos da base. Só os slugs enviados mudam (upsert parcial).
        Slug desconhecido → `UNKNOWN_FIELD_SLUG`.

        **Telefone:** preferir `country_code` + `phone_number` (só dígitos). Alternativa:
        string `phone` (o servidor normaliza; DDI padrão `55` se faltar).

        **Higiene:** lista de bloqueio → `CONTACT_BLOCKLISTED`; supressão global →
        `CONTACT_SUPPRESSED`. Contatos `unsubscribed` / `suppressed` não são reativados.

        HTTP **201** + `data.created=true` (criado) ou **200** + `created=false` (atualizado).
      parameters: []
      responses:
        200:
          description: atualizado
          content:
            application/json:
              schema:
                type: object
                example:
                  success: true
                  message: 'Contato atualizado via integração.'
                  data:
                    id: 1
                    email: helena@crm.test
                    created: false
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
                    example: 'Contato atualizado via integração.'
                  data:
                    type: object
                    properties:
                      id: { type: integer, example: 1 }
                      email: { type: string, example: helena@crm.test }
                      created: { type: boolean, example: false }
        201:
          description: criado
          content:
            application/json:
              schema:
                type: object
                example:
                  success: true
                  message: 'Contato criado via integração.'
                  data:
                    id: 1
                    email: helena@crm.test
                    name: Helena
                    status: active
                    tags:
                      - { id: 1, name: VIP }
                    fields:
                      empresa: Acme
                    created: true
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
                    example: 'Contato criado via integração.'
                  data:
                    type: object
                    properties:
                      id: { type: integer, example: 1 }
                      email: { type: string, example: helena@crm.test }
                      name: { type: string, example: Helena }
                      status: { type: string, example: active }
                      tags: { type: array, example: [{ id: 1, name: VIP }], items: { type: object, properties: { id: { type: integer, example: 1 }, name: { type: string, example: VIP } } } }
                      fields: { type: object, properties: { empresa: { type: string, example: Acme } } }
                      created: { type: boolean, example: true }
        401:
          description: sem_token
          content:
            application/json:
              schema:
                type: object
                example:
                  success: false
                  error:
                    code: UNAUTHORIZED
                    message: 'Autenticação necessária.'
                properties:
                  success:
                    type: boolean
                    example: false
                  error:
                    type: object
                    properties:
                      code: { type: string, example: UNAUTHORIZED }
                      message: { type: string, example: 'Autenticação necessária.' }
        422:
          description: ''
          content:
            application/json:
              schema:
                oneOf:
                  -
                    description: lista_bloqueio
                    type: object
                    example:
                      success: false
                      error: { code: CONTACT_BLOCKLISTED, message: 'Este e-mail ou domínio está na lista de bloqueio e não pode ser cadastrado.' }
                    properties:
                      success: { type: boolean, example: false }
                      error: { type: object, properties: { code: { type: string, example: CONTACT_BLOCKLISTED }, message: { type: string, example: 'Este e-mail ou domínio está na lista de bloqueio e não pode ser cadastrado.' } } }
                  -
                    description: supressao
                    type: object
                    example:
                      success: false
                      error: { code: CONTACT_SUPPRESSED, message: 'Este e-mail ou domínio está na lista de supressão e não pode ser cadastrado.' }
                    properties:
                      success: { type: boolean, example: false }
                      error: { type: object, properties: { code: { type: string, example: CONTACT_SUPPRESSED }, message: { type: string, example: 'Este e-mail ou domínio está na lista de supressão e não pode ser cadastrado.' } } }
                  -
                    description: campo_desconhecido
                    type: object
                    example:
                      success: false
                      error: { code: UNKNOWN_FIELD_SLUG, message: 'Slug de campo desconhecido.' }
                    properties:
                      success: { type: boolean, example: false }
                      error: { type: object, properties: { code: { type: string, example: UNKNOWN_FIELD_SLUG }, message: { type: string, example: 'Slug de campo desconhecido.' } } }
        429:
          description: rate_limit
          content:
            application/json:
              schema:
                type: object
                example:
                  success: false
                  error:
                    code: RATE_LIMIT_EXCEEDED
                    message: 'Limite de requisições excedido. Tente novamente em instantes.'
                properties:
                  success:
                    type: boolean
                    example: false
                  error:
                    type: object
                    properties:
                      code: { type: string, example: RATE_LIMIT_EXCEEDED }
                      message: { type: string, example: 'Limite de requisições excedido. Tente novamente em instantes.' }
      tags:
        - 'Integração de Contatos'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                email:
                  type: string
                  description: 'E-mail do contato (será normalizado em minúsculas).'
                  example: helena@crm.test
                name:
                  type: string
                  description: 'Nome de exibição.'
                  example: Helena
                  nullable: true
                country_code:
                  type: string
                  description: 'DDI só com dígitos (padrão 55 se vier telefone sem DDI).'
                  example: '55'
                  nullable: true
                phone_number:
                  type: string
                  description: 'Telefone só com dígitos (sem máscara).'
                  example: '11988887777'
                  nullable: true
                phone:
                  type: string
                  description: 'Alternativa: telefone em uma string; o servidor normaliza.'
                  example: '+55 11 98888-7777'
                  nullable: true
                tags:
                  type: array
                  description: 'Nomes das tags (cria se não existir). Com append anexa; com replace substitui o conjunto.'
                  example:
                    - 'Lead CRM'
                    - VIP
                  items:
                    type: string
                tag_match:
                  type: string
                  description: '`append` (padrão) ou `replace`.'
                  example: append
                fields:
                  type: object
                  description: 'Valores de campos tipados por slug do catálogo da conta.'
                  example:
                    empresa: Acme
                  properties: []
              required:
                - email
  /api/v1/integration/tags:
    get:
      summary: 'Listar tags'
      operationId: listarTags
      description: |-
        Retorna a lista paginada do catálogo. Use `search` para filtrar por nome
        (busca parcial, sem diferenciar maiúsculas/minúsculas) e `per_page` (1–100, padrão 15).
      parameters:
        -
          in: query
          name: search
          description: 'Filtro parcial no nome (sem diferenciar maiúsculas).'
          example: vip
          required: false
          schema:
            type: string
            description: 'Filtro parcial no nome (sem diferenciar maiúsculas).'
            example: vip
        -
          in: query
          name: per_page
          description: 'Itens por página (1–100, padrão 15).'
          example: 15
          required: false
          schema:
            type: integer
            description: 'Itens por página (1–100, padrão 15).'
            example: 15
      responses:
        200:
          description: ok
          content:
            application/json:
              schema:
                type: object
                example:
                  success: true
                  data:
                    - id: 1
                      name: VIP
                      contacts_count: 12
                  meta:
                    current_page: 1
                    per_page: 15
                    total: 1
                    last_page: 1
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    type: array
                    example:
                      - { id: 1, name: VIP, contacts_count: 12 }
                    items:
                      type: object
                      properties: { id: { type: integer, example: 1 }, name: { type: string, example: VIP }, contacts_count: { type: integer, example: 12 } }
                  meta:
                    type: object
                    properties:
                      current_page: { type: integer, example: 1 }
                      per_page: { type: integer, example: 15 }
                      total: { type: integer, example: 1 }
                      last_page: { type: integer, example: 1 }
      tags:
        - Tags
    post:
      summary: 'Criar tag'
      operationId: criarTag
      description: 'Cria um marcador pelo nome. Nome já existente (sem diferenciar maiúsculas) → validação 422.'
      parameters: []
      responses:
        201:
          description: criada
          content:
            application/json:
              schema:
                type: object
                example:
                  success: true
                  message: 'Tag criada.'
                  data:
                    id: 1
                    name: 'Lead CRM'
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
                    example: 'Tag criada.'
                  data:
                    type: object
                    properties:
                      id: { type: integer, example: 1 }
                      name: { type: string, example: 'Lead CRM' }
      tags:
        - Tags
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  description: 'Nome do marcador.'
                  example: 'Lead CRM'
              required:
                - name
  '/api/v1/integration/tags/{tag}':
    get:
      summary: 'Detalhe da tag'
      operationId: detalheDaTag
      description: 'Retorna id e nome. ID inexistente ou de outra conta → 404 (`NOT_FOUND`).'
      parameters: []
      responses:
        200:
          description: ok
          content:
            application/json:
              schema:
                type: object
                example:
                  success: true
                  data:
                    id: 1
                    name: VIP
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    type: object
                    properties:
                      id: { type: integer, example: 1 }
                      name: { type: string, example: VIP }
        404:
          description: nao_encontrada
          content:
            application/json:
              schema:
                type: object
                example:
                  success: false
                  error:
                    code: NOT_FOUND
                    message: 'Recurso não encontrado.'
                properties:
                  success:
                    type: boolean
                    example: false
                  error:
                    type: object
                    properties:
                      code: { type: string, example: NOT_FOUND }
                      message: { type: string, example: 'Recurso não encontrado.' }
      tags:
        - Tags
    put:
      summary: 'Renomear tag'
      operationId: renomearTag
      description: 'Altera apenas o nome. Os vínculos com contatos permanecem (só o rótulo muda).'
      parameters: []
      responses:
        200:
          description: ok
          content:
            application/json:
              schema:
                type: object
                example:
                  success: true
                  message: 'Tag atualizada.'
                  data:
                    id: 1
                    name: 'VIP 2026'
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
                    example: 'Tag atualizada.'
                  data:
                    type: object
                    properties:
                      id: { type: integer, example: 1 }
                      name: { type: string, example: 'VIP 2026' }
      tags:
        - Tags
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  description: 'Novo nome.'
                  example: 'VIP 2026'
              required:
                - name
    delete:
      summary: 'Excluir tag'
      operationId: excluirTag
      description: 'Remove a tag e os vínculos com contatos. Os **contatos** em si não são apagados.'
      parameters: []
      responses:
        200:
          description: ok
          content:
            application/json:
              schema:
                type: object
                example:
                  success: true
                  message: 'Tag excluída.'
                  data: null
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
                    example: 'Tag excluída.'
                  data:
                    type: string
                    example: null
                    nullable: true
      tags:
        - Tags
    parameters:
      -
        in: path
        name: tag
        description: 'ID da tag.'
        example: 1
        required: true
        schema:
          type: integer
  /api/v1/messages:
    post:
      summary: 'Enviar mensagem'
      operationId: enviarMensagem
      description: |-
        Aceita o envio depois de validar: setup de domínio, saúde da conta, higiene
        (lista de bloqueio / supressão), descadastro (só se `message_type=marketing`),
        cota do plano e capacidade da fila prioritária.

        Resposta **202**: `data.status` começa como `pending`. Consulte
        `GET /api/v1/messages/{id}` ou assine webhooks `message.sent` / `message.failed`
        (ver Getting Started → Receita C).

        Informe `html_body` **ou** `text_body` (pelo menos um).

        | `message_type` | Quando usar | Descadastro de marketing |
        |----------------|-------------|---------------------------|
        | `transactional` | Recibo, senha, NF, aviso operacional | Não aplica; ainda respeita blocklist/supressão |
        | `marketing` | Promoção / newsletter 1:1 | Respeita descadastro |
      parameters: []
      responses:
        202:
          description: aceito
          content:
            application/json:
              schema:
                type: object
                example:
                  success: true
                  message: 'Mensagem enfileirada. O envio começa em instantes.'
                  data:
                    id: 1
                    status: pending
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
                    example: 'Mensagem enfileirada. O envio começa em instantes.'
                  data:
                    type: object
                    properties:
                      id: { type: integer, example: 1 }
                      status: { type: string, example: pending }
        401:
          description: sem_token
          content:
            application/json:
              schema:
                type: object
                example:
                  success: false
                  error:
                    code: UNAUTHORIZED
                    message: 'Autenticação necessária.'
                properties:
                  success:
                    type: boolean
                    example: false
                  error:
                    type: object
                    properties:
                      code: { type: string, example: UNAUTHORIZED }
                      message: { type: string, example: 'Autenticação necessária.' }
        422:
          description: ''
          content:
            application/json:
              schema:
                oneOf:
                  -
                    description: cota
                    type: object
                    example:
                      success: false
                      error: { code: QUOTA_EXCEEDED, message: 'A cota de e-mails do ciclo acabou.' }
                    properties:
                      success: { type: boolean, example: false }
                      error: { type: object, properties: { code: { type: string, example: QUOTA_EXCEEDED }, message: { type: string, example: 'A cota de e-mails do ciclo acabou.' } } }
                  -
                    description: supressao
                    type: object
                    example:
                      success: false
                      error: { code: CONTACT_SUPPRESSED, message: 'Este e-mail ou domínio está na lista de supressão e não pode ser cadastrado.' }
                    properties:
                      success: { type: boolean, example: false }
                      error: { type: object, properties: { code: { type: string, example: CONTACT_SUPPRESSED }, message: { type: string, example: 'Este e-mail ou domínio está na lista de supressão e não pode ser cadastrado.' } } }
      tags:
        - 'Envio Avulso'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                to_email:
                  type: string
                  description: 'E-mail do destinatário (normalizado em minúsculas).'
                  example: cliente@empresa.test
                subject:
                  type: string
                  description: 'Assunto da mensagem.'
                  example: 'Seu pedido #123'
                html_body:
                  type: string
                  description: 'Corpo HTML (obrigatório se não enviar text_body).'
                  example: '<p>Olá, seu pedido saiu!</p>'
                  nullable: true
                text_body:
                  type: string
                  description: 'Corpo texto puro (obrigatório se não enviar html_body; convertido para HTML mínimo).'
                  example: 'Olá, seu pedido saiu!'
                  nullable: true
                message_type:
                  type: string
                  description: '`marketing` (respeita descadastro) ou `transactional` (recibo/senha).'
                  example: transactional
                from_email:
                  type: string
                  description: 'Remetente (deve usar o domínio de envio da conta; padrão = setup).'
                  example: ola@acme.test
                  nullable: true
                from_name:
                  type: string
                  description: 'Nome do remetente (padrão = setup).'
                  example: Acme
                  nullable: true
              required:
                - to_email
                - subject
                - message_type
  '/api/v1/messages/{message}':
    get:
      summary: 'Consultar status da mensagem'
      operationId: consultarStatusDaMensagem
      description: |-
        Retorna o status atual e timestamps.

        | Status | Significado |
        |--------|-------------|
        | `pending` | Aceito; ainda na fila ou em processamento |
        | `sent` | Enviado ao provedor com sucesso |
        | `failed` | Falhou no provedor / transporte |
        | `skipped` | Não enviado (ex.: higiene/regras no worker) |

        ID inexistente ou de outra conta → **404** (`NOT_FOUND`) — não revelamos dados cross-tenant.
      parameters: []
      responses:
        200:
          description: ok
          content:
            application/json:
              schema:
                type: object
                example:
                  success: true
                  data:
                    id: 1
                    status: pending
                    message_type: transactional
                    to_email: cliente@empresa.test
                    subject: 'Seu pedido #123'
                    scheduled_at: '2026-07-30T12:00:00+00:00'
                    sent_at: null
                    created_at: '2026-07-30T12:00:00+00:00'
                    updated_at: '2026-07-30T12:00:00+00:00'
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    type: object
                    properties:
                      id: { type: integer, example: 1 }
                      status: { type: string, example: pending }
                      message_type: { type: string, example: transactional }
                      to_email: { type: string, example: cliente@empresa.test }
                      subject: { type: string, example: 'Seu pedido #123' }
                      scheduled_at: { type: string, example: '2026-07-30T12:00:00+00:00' }
                      sent_at: { type: string, example: null, nullable: true }
                      created_at: { type: string, example: '2026-07-30T12:00:00+00:00' }
                      updated_at: { type: string, example: '2026-07-30T12:00:00+00:00' }
        404:
          description: nao_encontrada
          content:
            application/json:
              schema:
                type: object
                example:
                  success: false
                  error:
                    code: NOT_FOUND
                    message: 'Recurso não encontrado.'
                properties:
                  success:
                    type: boolean
                    example: false
                  error:
                    type: object
                    properties:
                      code: { type: string, example: NOT_FOUND }
                      message: { type: string, example: 'Recurso não encontrado.' }
      tags:
        - 'Envio Avulso'
    parameters:
      -
        in: path
        name: message
        description: 'ID retornado no POST.'
        example: 1
        required: true
        schema:
          type: integer
