openapi: 3.1.0
info:
  title: 4send API
  version: "1.0.0"
  description: |
    API pública do 4send — envie WhatsApp e e-mail, gerencie contatos, tags e
    campos personalizados, e coloque contatos em fluxos (jornadas automáticas).

    Autenticação por API key (cabeçalho `Authorization: Bearer <sua_key>`).
    Crie chaves em Integrações → Tokens de API. Chaves `4s_test_...` simulam o
    envio (não disparam de verdade); `4s_live_...` enviam pra valer.

    **Esta é uma API servidor-a-servidor: chame do seu backend, nunca do
    navegador.** A key é um segredo — em código de frontend ela fica visível
    para qualquer visitante da página, que passaria a enviar mensagens e ler os
    contatos da conta. Chamadas com `4s_live_...` vindas de um navegador são
    recusadas com `403`; `4s_test_...` é liberada no navegador por ser sandbox,
    para você prototipar a tela antes de mover a chamada pro servidor.
  contact:
    name: Suporte 4send
    url: https://app.4send.me
servers:
  - url: https://api.4send.me/v1
    description: Produção
security:
  - ApiKey: []
tags:
  - name: Mensagens
  - name: Contatos
  - name: Fluxos
  - name: Tags
  - name: Campos personalizados
  - name: Conta

paths:
  /:
    get:
      tags: [Conta]
      summary: Verificar a chave
      description: Confirma que a chave é válida e diz o modo (test/live).
      responses:
        "200":
          description: Chave válida
          content:
            application/json:
              example: { ok: true, version: "v1", workspace: "6a60...", mode: "live" }

  /messages:
    post:
      tags: [Mensagens]
      summary: Enviar mensagem (WhatsApp ou e-mail)
      description: |
        Envio transacional. Responde na hora com `202` e o status `queued`;
        o resultado final chega por webhook ou consultando `GET /messages/{id}`.
        Suporta o cabeçalho `Idempotency-Key` para evitar envio duplicado.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/MessageCreate" }
            examples:
              whatsapp:
                summary: WhatsApp com variável
                value:
                  channel: whatsapp
                  to: { phone: "+5551999999999" }
                  whatsapp: { body: "Olá {{nome}}! Seu pedido saiu 🚚" }
                  variables: { nome: "João" }
              email:
                summary: E-mail
                value:
                  channel: email
                  to: { email: "joao@exemplo.com" }
                  email:
                    subject: "Seu pedido saiu"
                    html: "<h1>Olá, {{nome}}!</h1><p>Seu pedido está a caminho.</p>"
                  variables: { nome: "João" }
      responses:
        "202":
          description: Aceito (na fila)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Message" }
        "400": { $ref: "#/components/responses/Error" }
        "401": { $ref: "#/components/responses/Error" }
        "403": { $ref: "#/components/responses/Error" }
        "404": { $ref: "#/components/responses/Error" }
        "409": { $ref: "#/components/responses/Error" }
        "429": { $ref: "#/components/responses/Error" }

  /messages/{id}:
    get:
      tags: [Mensagens]
      summary: Ver status da mensagem
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, example: "msg_663f0a..." }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Message" }
        "404": { $ref: "#/components/responses/Error" }

  /contacts:
    post:
      tags: [Contatos]
      summary: Criar / atualizar contato
      description: Deduplica por telefone; na falta, por e-mail. Campos omitidos não são apagados.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ContactCreate" }
            example:
              phone: "+5551999999999"
              name: "João"
              tags: ["tag_663f..."]
              fields: { plano: "gold" }
      responses:
        "200":
          description: Contato criado ou atualizado
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Contact" }
        "400": { $ref: "#/components/responses/Error" }
    get:
      tags: [Contatos]
      summary: Buscar contato por telefone ou e-mail
      parameters:
        - { name: phone, in: query, schema: { type: string } }
        - { name: email, in: query, schema: { type: string } }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Contact" }
        "404": { $ref: "#/components/responses/Error" }

  /contacts/{id}:
    get:
      tags: [Contatos]
      summary: Buscar contato por id
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, example: "ct_663f..." } }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Contact" }
        "404": { $ref: "#/components/responses/Error" }

  /flows:
    get:
      tags: [Fluxos]
      summary: Listar fluxos ativos
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Flow" }

  /flows/{flow_id}/enroll:
    post:
      tags: [Fluxos]
      summary: Colocar um contato num fluxo
      parameters:
        - { name: flow_id, in: path, required: true, schema: { type: string, example: "flow_663f..." } }
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                to: { $ref: "#/components/schemas/Recipient" }
                variables: { type: object, additionalProperties: true }
            example:
              to: { phone: "+5551999999999" }
              variables: { nome: "João", codigo: "A1B2" }
      responses:
        "201":
          description: Contato matriculado
          content:
            application/json:
              schema: { $ref: "#/components/schemas/FlowRun" }
        "404": { $ref: "#/components/responses/Error" }
        "409": { $ref: "#/components/responses/Error" }

  /flows/{flow_id}/unenroll:
    post:
      tags: [Fluxos]
      summary: Tirar um contato do fluxo
      description: |
        Interrompe a jornada do contato: ele para de receber o que faltava.
        Escopo `flows:enroll` (a mesma permissão de matricular).

        Use `flow_id: all` para tirar o contato de **todas** as jornadas em que
        ele estiver — é o caso mais comum de quem integra ("comprou, para tudo").

        É idempotente: se ele não estiver em jornada nenhuma, responde `200` com
        `unenrolled: 0` em vez de erro. Chamar "pare de mandar" tem que ser
        seguro sempre.
      parameters:
        - name: flow_id
          in: path
          required: true
          schema: { type: string, example: "flow_663f..." }
          description: 'Id do fluxo, ou o valor especial `all` para sair de todas as jornadas.'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                to: { $ref: "#/components/schemas/Recipient" }
                reason:
                  type: string
                  description: Por que saiu. Vai no relatório e no webhook `fluxo.parado`.
                  example: comprou
            example:
              to: { phone: "+5551999999999" }
              reason: comprou
      responses:
        "200":
          description: Jornadas interrompidas (pode ser 0)
          content:
            application/json:
              example:
                unenrolled: 1
                contact_id: "ct_663f..."
                flow_id: "flow_663f..."
                runs:
                  - { id: "run_663f...", flow_id: "flow_663f...", status: stopped, reason: comprou }
        "400": { $ref: "#/components/responses/Error" }
        "404": { $ref: "#/components/responses/Error" }

  /tags:
    get:
      tags: [Tags]
      summary: Listar tags
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { type: array, items: { $ref: "#/components/schemas/Tag" } }
    post:
      tags: [Tags]
      summary: Criar tag
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string, example: "cliente-vip" }
                color: { type: string, example: "#FF4E33" }
      responses:
        "201":
          description: Tag criada
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Tag" }
        "409": { $ref: "#/components/responses/Error" }

  /custom-fields:
    get:
      tags: [Campos personalizados]
      summary: Listar campos personalizados
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { type: array, items: { $ref: "#/components/schemas/CustomField" } }
    post:
      tags: [Campos personalizados]
      summary: Criar campo personalizado
      description: A `key` vira a variável `{{key}}` nas mensagens.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, key]
              properties:
                name: { type: string, example: "Plano" }
                key: { type: string, example: "plano" }
                type: { type: string, enum: [text, number, date, boolean], default: text }
      responses:
        "201":
          description: Campo criado
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CustomField" }
        "409": { $ref: "#/components/responses/Error" }

  /usage:
    get:
      tags: [Conta]
      summary: Consumo do mês
      responses:
        "200":
          description: OK
          content:
            application/json:
              example:
                period: "2026-08"
                whatsapp: 1234
                email: 567
                total: 1801
                limit: null
                daily_limit_per_channel: 1000

components:
  securitySchemes:
    ApiKey:
      type: http
      scheme: bearer
      description: >-
        Sua API key (4s_live_... ou 4s_test_...) no cabeçalho Authorization.
        Use SEMPRE a partir do seu servidor (backend) — nunca de código que roda
        no navegador, onde a key fica visível para qualquer visitante. Chamada
        com 4s_live_ vinda de um navegador é recusada com 403.
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: UUID gerado pelo cliente. Repetir a mesma chamada com a mesma key não duplica o envio.
      schema: { type: string, example: "550e8400-e29b-41d4-a716-446655440000" }
  responses:
    Error:
      description: Erro
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example:
            error:
              type: invalid_request
              message: "O contato não tem telefone p/ WhatsApp."
              param: "to.phone"
              doc_url: "https://docs.4send.me"
  schemas:
    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            type:
              type: string
              description: "authentication | forbidden | invalid_request | not_found | conflict | rate_limit | quota_exceeded | api_error"
            message: { type: string }
            param: { type: string }
            doc_url: { type: string }
    Recipient:
      type: object
      description: Destinatário — informe phone, email OU contact_id.
      properties:
        phone: { type: string, example: "+5551999999999" }
        email: { type: string, example: "joao@exemplo.com" }
        contact_id: { type: string, example: "ct_663f..." }
        name: { type: string }
    MessageCreate:
      type: object
      required: [channel, to]
      properties:
        channel: { type: string, enum: [whatsapp, email] }
        to: { $ref: "#/components/schemas/Recipient" }
        whatsapp:
          type: object
          properties:
            body: { type: string, description: "Texto (aceita {{variaveis}})" }
            media_url: { type: string }
            media_type: { type: string, enum: [image, video, audio, document] }
        email:
          type: object
          properties:
            subject: { type: string }
            html: { type: string }
            reply_to: { type: string }
            preview_text: { type: string }
        variables:
          type: object
          additionalProperties: true
          description: Valores das {{variaveis}} do texto.
        channel_id:
          type: string
          description: Canal específico (senão usa o conectado do tipo).
    Message:
      type: object
      properties:
        id: { type: string, example: "msg_663f0a..." }
        status: { type: string, enum: [queued, sent, delivered, read, failed] }
        channel: { type: string, enum: [whatsapp, email] }
        to: { type: string, nullable: true }
        provider_message_id: { type: string, nullable: true }
        error: { type: string }
        sent_at: { type: string, nullable: true }
        delivered_at: { type: string, nullable: true }
        read_at: { type: string, nullable: true }
        created_at: { type: string }
    ContactCreate:
      type: object
      properties:
        phone: { type: string }
        email: { type: string }
        name: { type: string }
        tags: { type: array, items: { type: string }, description: "ids de tag (tag_...)" }
        fields: { type: object, additionalProperties: true }
        opt_out_whatsapp: { type: boolean }
        opt_out_email: { type: boolean }
    Contact:
      type: object
      properties:
        id: { type: string, example: "ct_663f..." }
        phone: { type: string, nullable: true }
        email: { type: string, nullable: true }
        name: { type: string, nullable: true }
        tags: { type: array, items: { type: string } }
        fields: { type: object, additionalProperties: true }
        opt_out_whatsapp: { type: boolean }
        opt_out_email: { type: boolean }
        created_at: { type: string }
        updated_at: { type: string }
    Flow:
      type: object
      properties:
        id: { type: string, example: "flow_663f..." }
        name: { type: string }
        status: { type: string, example: "active" }
    FlowRun:
      type: object
      properties:
        id: { type: string, example: "run_663f..." }
        flow_id: { type: string, example: "flow_663f..." }
        contact_id: { type: string, example: "ct_663f..." }
        status: { type: string, example: "active" }
        created_at: { type: string }
    Tag:
      type: object
      properties:
        id: { type: string, example: "tag_663f..." }
        name: { type: string }
        color: { type: string, nullable: true }
        created_at: { type: string }
    CustomField:
      type: object
      properties:
        id: { type: string, example: "field_663f..." }
        name: { type: string }
        key: { type: string }
        type: { type: string, enum: [text, number, date, boolean] }
        created_at: { type: string }
