openapi: 3.1.0
info:
  title: TDFácil Public API
  version: 1.0.0
  description: |
    Contrato oficial da API pública TDFÁCIL, disponível em produção sob
    `https://api.tdfacil.com/v1`.

    A disponibilidade de cada operação depende dos recursos habilitados para a
    empresa e dos escopos concedidos à credencial.

    Todas as mutações, incluindo preview, exigem `Idempotency-Key`. A mesma
    chave, rota e corpo reproduzem a resposta armazenada; reutilização com
    corpo diferente retorna `409`.

    O limite global de corpo Fastify é 256 KiB. Limites atuais por minuto são
    leitura 120, preview 10 e mutação 10, reduzidos pelo limite configurado da
    credencial e aplicados simultaneamente à identidade e ao IP. Autenticações
    inválidas são limitadas a 30/min por IP e previews a 100/dia por tenant.
    Operações pagas admitem no máximo 20 grupos pendentes por credencial e 100
    por tenant; o scheduler processa no máximo 2 jobs por credencial e 10 por
    tenant. Respostas incluem `X-Request-Id`; admissão inclui headers
    `X-RateLimit-*` e limites excedidos retornam `429` com `Retry-After`.
servers:
  - url: https://api.tdfacil.com
    description: Produção
tags:
  - name: Wallet
  - name: Usage
  - name: References
  - name: Leads
  - name: Operations
  - name: Webhooks
security:
  - bearerApiKey: []
paths:
  /v1/balance:
    get:
      tags: [Wallet]
      operationId: getBalance
      summary: Consulta a carteira de créditos do tenant
      x-required-scope: balance:read
      responses:
        "200":
          description: Saldo atual
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BalanceResponse"
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /v1/usage:
    get:
      tags: [Usage]
      operationId: listUsage
      summary: Lista eventos de consumo do tenant
      x-required-scope: usage:read
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: Página de eventos de uso
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UsagePage"
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /v1/references/{catalog}:
    get:
      tags: [References]
      operationId: listReferences
      summary: Pesquisa CNAEs ou municípios brasileiros
      x-required-scope: references:read
      parameters:
        - name: catalog
          in: path
          required: true
          schema:
            type: string
            enum: [cnaes, municipios]
        - name: search
          in: query
          required: false
          schema:
            type: string
            minLength: 1
            maxLength: 120
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: Página do catálogo
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ReferencePage"
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/ValidationError" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /v1/lead-search-previews:
    post:
      tags: [Leads]
      operationId: previewLeadSearch
      summary: Estima uma busca sem criar lista paga
      description: |
        Credenciais `test` recebem fixture determinística. Para credenciais
        `live`, a estimativa consulta a fonte de dados empresarial configurada.
        Quando a consulta estiver desabilitada ou temporariamente indisponível,
        retorna `503 lead_search_preview_unavailable`.
      x-required-scope: leads:read
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LeadSearchPreviewInput"
      responses:
        "200":
          description: Estimativa ou replay idempotente
          headers:
            Idempotent-Replayed:
              $ref: "#/components/headers/IdempotentReplayed"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LeadSearchPreviewResponse"
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/Conflict" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "422": { $ref: "#/components/responses/ValidationError" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /v1/lead-lists:
    post:
      tags: [Leads]
      operationId: createLeadList
      summary: Reserva créditos e cria uma lista assíncrona
      x-required-scope: leads:write
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateLeadListInput"
      responses:
        "202":
          description: Operação aceita
          headers:
            Location:
              description: URL relativa da operação
              schema: { type: string }
            Idempotent-Replayed:
              $ref: "#/components/headers/IdempotentReplayed"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AcceptedLeadListResponse"
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/PaymentRequired" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/Conflict" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "422": { $ref: "#/components/responses/ValidationError" }
        "429": { $ref: "#/components/responses/RateLimited" }
    get:
      tags: [Leads]
      operationId: listLeadLists
      summary: Lista as listas do tenant
      x-required-scope: leads:read
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum: [draft, previewing, generating, completed, failed]
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: Página de listas
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LeadListPage"
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /v1/lead-lists/{id}:
    get:
      tags: [Leads]
      operationId: getLeadList
      summary: Consulta uma lista do tenant
      x-required-scope: leads:read
      parameters:
        - $ref: "#/components/parameters/UuidPath"
      responses:
        "200":
          description: Lista
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: "#/components/schemas/LeadList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /v1/lead-lists/{id}/leads:
    get:
      tags: [Leads]
      operationId: listLeads
      summary: Lista a projeção segura dos leads de uma lista
      x-required-scope: leads:read
      parameters:
        - $ref: "#/components/parameters/UuidPath"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: Página de leads
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LeadPage"
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /v1/enrichments:
    post:
      tags: [Leads]
      operationId: createEnrichment
      summary: Solicita enriquecimento assíncrono de um lead
      x-required-scope: enrichments:write
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateEnrichmentInput"
      responses:
        "202":
          description: Operação aceita
          headers:
            Location:
              description: URL relativa da operação
              schema: { type: string }
            Idempotent-Replayed:
              $ref: "#/components/headers/IdempotentReplayed"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AcceptedOperationResponse"
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/PaymentRequired" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/Conflict" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "422": { $ref: "#/components/responses/ValidationError" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /v1/operations/{id}:
    get:
      tags: [Operations]
      operationId: getOperation
      summary: Consulta uma operação assíncrona
      x-required-scope: operations:read
      parameters:
        - $ref: "#/components/parameters/UuidPath"
      responses:
        "200":
          description: Operação
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: "#/components/schemas/Operation" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /v1/operations/{id}/cancel:
    post:
      tags: [Operations]
      operationId: cancelOperation
      summary: Cancela uma operação se seu estado permitir
      x-required-scope: operations:read
      parameters:
        - $ref: "#/components/parameters/UuidPath"
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "202":
          description: Cancelamento aceito ou replayado
          headers:
            Location:
              description: URL relativa da operação
              schema: { type: string }
            Idempotent-Replayed:
              $ref: "#/components/headers/IdempotentReplayed"
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: "#/components/schemas/Operation" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /v1/webhook-endpoints:
    post:
      tags: [Webhooks]
      operationId: createWebhookEndpoint
      summary: Registra um endpoint HTTPS
      description: O `signing_secret` só aparece na resposta de criação/replay idempotente.
      x-required-scope: webhooks:manage
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateWebhookEndpointInput"
      responses:
        "201":
          description: Endpoint criado
          headers:
            Location:
              description: URL relativa do endpoint
              schema: { type: string }
            Idempotent-Replayed:
              $ref: "#/components/headers/IdempotentReplayed"
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: "#/components/schemas/CreatedWebhookEndpoint" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/Conflict" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "422": { $ref: "#/components/responses/ValidationError" }
        "429": { $ref: "#/components/responses/RateLimited" }
    get:
      tags: [Webhooks]
      operationId: listWebhookEndpoints
      summary: Lista endpoints registrados
      x-required-scope: webhooks:manage
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: Página de endpoints
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookEndpointPage"
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /v1/webhook-endpoints/{id}:
    get:
      tags: [Webhooks]
      operationId: getWebhookEndpoint
      summary: Consulta um endpoint sem revelar segredo
      x-required-scope: webhooks:manage
      parameters:
        - $ref: "#/components/parameters/UuidPath"
      responses:
        "200":
          description: Endpoint
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: "#/components/schemas/WebhookEndpoint" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
    patch:
      tags: [Webhooks]
      operationId: updateWebhookEndpoint
      summary: Atualiza ou habilita/desabilita um endpoint
      x-required-scope: webhooks:manage
      parameters:
        - $ref: "#/components/parameters/UuidPath"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateWebhookEndpointInput"
      responses:
        "200":
          description: Endpoint atualizado
          headers:
            Idempotent-Replayed:
              $ref: "#/components/headers/IdempotentReplayed"
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: "#/components/schemas/WebhookEndpoint" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "422": { $ref: "#/components/responses/ValidationError" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      tags: [Webhooks]
      operationId: deleteWebhookEndpoint
      summary: Exclui logicamente um endpoint
      x-required-scope: webhooks:manage
      parameters:
        - $ref: "#/components/parameters/UuidPath"
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "204":
          description: Endpoint excluído
          headers:
            Idempotent-Replayed:
              $ref: "#/components/headers/IdempotentReplayed"
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "429": { $ref: "#/components/responses/RateLimited" }
components:
  securitySchemes:
    bearerApiKey:
      type: http
      scheme: bearer
      bearerFormat: TDFácil API key
      description: Chave `tdf_live_...` ou `tdf_test_...`, não JWT.
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      schema:
        type: string
        minLength: 8
        maxLength: 128
        pattern: "^[A-Za-z0-9._:-]+$"
    Limit:
      name: limit
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        maximum: 200
        default: 50
    Cursor:
      name: cursor
      in: query
      required: false
      schema:
        type: string
        minLength: 1
        maxLength: 1024
    UuidPath:
      name: id
      in: path
      required: true
      schema: { type: string, format: uuid }
  headers:
    IdempotentReplayed:
      description: Presente e `true` quando a resposta veio do registro idempotente.
      schema: { type: string, const: "true" }
  responses:
    BadRequest:
      description: JSON inválido ou Idempotency-Key ausente/inválida
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
          example:
            type: about:blank
            title: Requisição inválida
            status: 400
            detail: O cabeçalho Idempotency-Key é obrigatório.
            instance: /v1/lead-lists
            code: invalid_idempotency_key
            request_id: req_demo_400
    Unauthorized:
      description: Credencial ausente, inválida, expirada ou revogada
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
          example:
            type: about:blank
            title: Não autorizado
            status: 401
            detail: A credencial informada é inválida, expirou ou foi revogada.
            instance: /v1/balance
            code: invalid_api_key
            request_id: req_demo_401
    PaymentRequired:
      description: Saldo ou limite diário da credencial insuficiente
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
          example:
            type: about:blank
            title: Créditos insuficientes
            status: 402
            detail: A empresa não possui créditos disponíveis para esta operação.
            instance: /v1/enrichments
            code: insufficient_credits
            request_id: req_demo_402
    Forbidden:
      description: Escopo/tenant não autorizado ou credencial de teste tentou mutar dados reais
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
          example:
            type: about:blank
            title: Acesso negado
            status: 403
            detail: A credencial não possui o escopo exigido por esta operação.
            instance: /v1/usage
            code: insufficient_scope
            request_id: req_demo_403
    NotFound:
      description: Recurso não encontrado dentro do tenant autenticado
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
          example:
            type: about:blank
            title: Recurso não encontrado
            status: 404
            detail: O recurso não existe ou não pertence à empresa autenticada.
            instance: /v1/operations/00000000-0000-4000-8000-000000000404
            code: resource_not_found
            request_id: req_demo_404
    Conflict:
      description: Conflito de estado ou idempotência
      headers:
        Retry-After:
          schema: { type: integer, minimum: 1 }
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
          example:
            type: about:blank
            title: Conflito
            status: 409
            detail: A Idempotency-Key já foi utilizada com outro conteúdo.
            instance: /v1/lead-lists
            code: idempotency_conflict
            request_id: req_demo_409
    ValidationError:
      description: Campos inválidos
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
          example:
            type: about:blank
            title: Dados inválidos
            status: 422
            detail: Um ou mais campos não passaram pela validação.
            instance: /v1/webhook-endpoints
            code: validation_error
            request_id: req_demo_422
            errors:
              - path: url
                message: Informe uma URL HTTPS válida.
    PayloadTooLarge:
      description: Corpo HTTP acima de 256 KiB
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
          example:
            type: about:blank
            title: Corpo muito grande
            status: 413
            detail: O corpo da requisição excede o limite de 256 KiB.
            instance: /v1/lead-lists
            code: payload_too_large
            request_id: req_demo_413
    RateLimited:
      description: Limite excedido
      headers:
        Retry-After:
          schema: { type: integer, minimum: 1 }
        X-RateLimit-Limit:
          schema: { type: integer }
        X-RateLimit-Remaining:
          schema: { type: integer }
        X-RateLimit-Reset:
          schema: { type: integer, description: Epoch em segundos }
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
          example:
            type: about:blank
            title: Limite excedido
            status: 429
            detail: Aguarde antes de realizar uma nova requisição.
            instance: /v1/lead-search-previews
            code: rate_limit_exceeded
            request_id: req_demo_429
            metadata:
              retry_after_seconds: 30
    Unavailable:
      description: Dependência ou superfície temporariamente indisponível
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
          example:
            type: about:blank
            title: Serviço temporariamente indisponível
            status: 503
            detail: Não foi possível concluir a consulta neste momento.
            instance: /v1/lead-search-previews
            code: service_unavailable
            request_id: req_demo_503
  schemas:
    JsonValue:
      oneOf:
        - type: string
        - type: number
        - type: boolean
        - type: "null"
        - type: array
          items: { $ref: "#/components/schemas/JsonValue" }
        - type: object
          additionalProperties: { $ref: "#/components/schemas/JsonValue" }
    JsonObject:
      type: object
      additionalProperties: { $ref: "#/components/schemas/JsonValue" }
    PageMeta:
      type: object
      required: [next_cursor]
      properties:
        next_cursor: { type: [string, "null"] }
    Balance:
      type: object
      required: [balance, reserved, available, version, updated_at]
      properties:
        balance: { type: integer, format: int64 }
        reserved: { type: integer, format: int64 }
        available: { type: integer, format: int64 }
        version: { type: string }
        updated_at: { type: string, format: date-time }
    BalanceResponse:
      type: object
      required: [data]
      properties:
        data: { $ref: "#/components/schemas/Balance" }
      example:
        data:
          balance: 1200
          reserved: 80
          available: 1120
          version: "42"
          updated_at: "2026-08-02T14:30:00Z"
    UsageEvent:
      type: object
      required: [id, metric, quantity, unit_amount, cost, status, occurred_at, operation_id, dimensions]
      properties:
        id: { type: string }
        metric: { type: string }
        quantity: { type: integer }
        unit_amount: { type: number }
        cost: { type: integer, format: int64 }
        status: { type: string }
        occurred_at: { type: string, format: date-time }
        operation_id: { type: [string, "null"], format: uuid }
        dimensions: { $ref: "#/components/schemas/JsonObject" }
    UsagePage:
      type: object
      required: [data, meta]
      properties:
        data: { type: array, items: { $ref: "#/components/schemas/UsageEvent" } }
        meta: { $ref: "#/components/schemas/PageMeta" }
      example:
        data:
          - id: usage_demo_001
            metric: lead_list.created
            quantity: 50
            unit_amount: 1
            cost: 50
            status: captured
            occurred_at: "2026-08-02T14:20:00Z"
            operation_id: 10000000-0000-4000-8000-000000000001
            dimensions:
              lead_list_id: 20000000-0000-4000-8000-000000000001
        meta:
          next_cursor: cursor_demo_002
    ReferenceItem:
      type: object
      required: [id, label, metadata]
      properties:
        id: { type: string }
        label: { type: string }
        metadata: { $ref: "#/components/schemas/JsonObject" }
    ReferencePage:
      type: object
      required: [data, meta]
      properties:
        data: { type: array, items: { $ref: "#/components/schemas/ReferenceItem" } }
        meta: { $ref: "#/components/schemas/PageMeta" }
      example:
        data:
          - id: "6201501"
            label: Desenvolvimento de programas de computador sob encomenda
            metadata:
              section: J
        meta:
          next_cursor: null
    LeadSearchPreviewInput:
      type: object
      additionalProperties: false
      required: [filters]
      properties:
        filters: { $ref: "#/components/schemas/JsonObject" }
        limit: { type: integer, minimum: 1, maximum: 10000, default: 100 }
      example:
        filters:
          state: SP
          city: Campinas
          cnae: ["6201501"]
        limit: 100
    LeadSearchPreview:
      type: object
      required: [estimated_count, capped_at, filters]
      properties:
        estimated_count: { type: integer, minimum: 0 }
        capped_at: { type: [integer, "null"] }
        filters: { $ref: "#/components/schemas/JsonObject" }
    LeadSearchPreviewResponse:
      type: object
      required: [data]
      properties:
        data: { $ref: "#/components/schemas/LeadSearchPreview" }
      example:
        data:
          estimated_count: 684
          capped_at: 10000
          filters:
            state: SP
            city: Campinas
            cnae: ["6201501"]
    CreateLeadListInput:
      type: object
      additionalProperties: false
      required: [title, filters, limit]
      properties:
        title: { type: string, minLength: 1, maxLength: 160 }
        description: { type: string, maxLength: 1000 }
        filters: { $ref: "#/components/schemas/JsonObject" }
        limit: { type: integer, minimum: 1, maximum: 1000 }
        priority: { type: integer, minimum: -100, maximum: 100, default: 0 }
      example:
        title: Empresas de tecnologia em Campinas
        description: Lista para a campanha comercial do terceiro trimestre
        filters:
          state: SP
          city: Campinas
          cnae: ["6201501"]
        limit: 100
        priority: 0
    LeadList:
      type: object
      required: [id, title, description, status, import_status, total_leads, imported_count, excluded_count, credits_consumed, is_enriched, filters, created_at, updated_at]
      properties:
        id: { type: string, format: uuid }
        title: { type: string }
        description: { type: [string, "null"] }
        status: { type: string }
        import_status: { type: string }
        total_leads: { type: integer }
        imported_count: { type: integer }
        excluded_count: { type: integer }
        credits_consumed: { type: integer, format: int64 }
        is_enriched: { type: boolean }
        filters: { $ref: "#/components/schemas/JsonObject" }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
      example:
        id: 20000000-0000-4000-8000-000000000001
        title: Empresas de tecnologia em Campinas
        description: Lista para a campanha comercial do terceiro trimestre
        status: processing
        import_status: importing
        total_leads: 100
        imported_count: 36
        excluded_count: 4
        credits_consumed: 36
        is_enriched: false
        filters:
          state: SP
          city: Campinas
          cnae: ["6201501"]
        created_at: "2026-08-02T14:20:00Z"
        updated_at: "2026-08-02T14:24:00Z"
    LeadListPage:
      type: object
      required: [data, meta]
      properties:
        data: { type: array, items: { $ref: "#/components/schemas/LeadList" } }
        meta: { $ref: "#/components/schemas/PageMeta" }
      example:
        data:
          - id: 20000000-0000-4000-8000-000000000001
            title: Empresas de tecnologia em Campinas
            description: Lista para a campanha comercial do terceiro trimestre
            status: completed
            import_status: completed
            total_leads: 100
            imported_count: 96
            excluded_count: 4
            credits_consumed: 96
            is_enriched: false
            filters: { state: SP, city: Campinas, cnae: ["6201501"] }
            created_at: "2026-08-02T14:20:00Z"
            updated_at: "2026-08-02T14:40:00Z"
        meta:
          next_cursor: cursor_demo_lists_002
    Lead:
      type: object
      required: [id, list_id, cnpj, razao_social, nome_fantasia, situacao_cadastral, porte, cnae_principal, cnae_principal_descricao, municipio, uf, email, telefone, status, created_at]
      properties:
        id: { type: string, format: uuid }
        list_id: { type: string, format: uuid }
        cnpj: { type: string }
        razao_social: { type: [string, "null"] }
        nome_fantasia: { type: [string, "null"] }
        situacao_cadastral: { type: [string, "null"] }
        porte: { type: [string, "null"] }
        cnae_principal: { type: [string, "null"] }
        cnae_principal_descricao: { type: [string, "null"] }
        municipio: { type: [string, "null"] }
        uf: { type: [string, "null"] }
        email: { type: [string, "null"] }
        telefone: { type: [string, "null"] }
        status: { type: string }
        created_at: { type: string, format: date-time }
      example:
        id: 30000000-0000-4000-8000-000000000001
        list_id: 20000000-0000-4000-8000-000000000001
        cnpj: "12345678000195"
        razao_social: Empresa Exemplo Tecnologia Ltda
        nome_fantasia: Exemplo Tech
        situacao_cadastral: ATIVA
        porte: PEQUENA EMPRESA
        cnae_principal: "6201501"
        cnae_principal_descricao: Desenvolvimento de programas de computador sob encomenda
        municipio: Campinas
        uf: SP
        email: contato@example.com
        telefone: "+551930000000"
        status: imported
        created_at: "2026-08-02T14:21:00Z"
    LeadPage:
      type: object
      required: [data, meta]
      properties:
        data: { type: array, items: { $ref: "#/components/schemas/Lead" } }
        meta: { $ref: "#/components/schemas/PageMeta" }
      example:
        data:
          - id: 30000000-0000-4000-8000-000000000001
            list_id: 20000000-0000-4000-8000-000000000001
            cnpj: "12345678000195"
            razao_social: Empresa Exemplo Tecnologia Ltda
            nome_fantasia: Exemplo Tech
            situacao_cadastral: ATIVA
            porte: PEQUENA EMPRESA
            cnae_principal: "6201501"
            cnae_principal_descricao: Desenvolvimento de programas de computador sob encomenda
            municipio: Campinas
            uf: SP
            email: contato@example.com
            telefone: "+551930000000"
            status: imported
            created_at: "2026-08-02T14:21:00Z"
        meta:
          next_cursor: null
    CreateEnrichmentInput:
      type: object
      additionalProperties: false
      required: [lead_id]
      properties:
        lead_id: { type: string, format: uuid }
        kind: { type: string, const: company_people, default: company_people }
      example:
        lead_id: 30000000-0000-4000-8000-000000000001
        kind: company_people
    OperationError:
      type: object
      required: [code, message]
      properties:
        code: { type: string }
        message: { type: string }
    Operation:
      type: object
      required: [id, type, status, resource_type, resource_id, cost, reserved_cost, captured_cost, progress, error, created_at, started_at, finished_at, updated_at]
      properties:
        id: { type: string, format: uuid }
        type: { type: string }
        status: { type: string }
        resource_type: { type: [string, "null"] }
        resource_id: { type: [string, "null"] }
        cost: { type: integer, format: int64 }
        reserved_cost: { type: integer, format: int64 }
        captured_cost: { type: integer, format: int64 }
        progress: { type: [number, "null"] }
        error:
          oneOf:
            - $ref: "#/components/schemas/OperationError"
            - type: "null"
        created_at: { type: string, format: date-time }
        started_at: { type: [string, "null"], format: date-time }
        finished_at: { type: [string, "null"], format: date-time }
        updated_at: { type: string, format: date-time }
      example:
        id: 10000000-0000-4000-8000-000000000001
        type: lead.enrichment
        status: processing
        resource_type: lead
        resource_id: 30000000-0000-4000-8000-000000000001
        cost: 1
        reserved_cost: 1
        captured_cost: 0
        progress: 0.35
        error: null
        created_at: "2026-08-02T14:30:00Z"
        started_at: "2026-08-02T14:30:01Z"
        finished_at: null
        updated_at: "2026-08-02T14:30:08Z"
    AcceptedOperation:
      type: object
      required: [operation]
      properties:
        operation: { $ref: "#/components/schemas/Operation" }
    AcceptedOperationResponse:
      type: object
      required: [data]
      properties:
        data: { $ref: "#/components/schemas/AcceptedOperation" }
      example:
        data:
          operation:
            id: 10000000-0000-4000-8000-000000000001
            type: lead.enrichment
            status: queued
            resource_type: lead
            resource_id: 30000000-0000-4000-8000-000000000001
            cost: 1
            reserved_cost: 1
            captured_cost: 0
            progress: 0
            error: null
            created_at: "2026-08-02T14:30:00Z"
            started_at: null
            finished_at: null
            updated_at: "2026-08-02T14:30:00Z"
    AcceptedLeadList:
      type: object
      required: [operation]
      properties:
        operation: { $ref: "#/components/schemas/Operation" }
        resource: { $ref: "#/components/schemas/LeadList" }
    AcceptedLeadListResponse:
      type: object
      required: [data]
      properties:
        data: { $ref: "#/components/schemas/AcceptedLeadList" }
      example:
        data:
          operation:
            id: 10000000-0000-4000-8000-000000000002
            type: lead_list.create
            status: queued
            resource_type: lead_list
            resource_id: 20000000-0000-4000-8000-000000000001
            cost: 100
            reserved_cost: 100
            captured_cost: 0
            progress: 0
            error: null
            created_at: "2026-08-02T14:20:00Z"
            started_at: null
            finished_at: null
            updated_at: "2026-08-02T14:20:00Z"
          resource:
            id: 20000000-0000-4000-8000-000000000001
            title: Empresas de tecnologia em Campinas
            description: Lista para a campanha comercial do terceiro trimestre
            status: queued
            import_status: pending
            total_leads: 100
            imported_count: 0
            excluded_count: 0
            credits_consumed: 0
            is_enriched: false
            filters: { state: SP, city: Campinas, cnae: ["6201501"] }
            created_at: "2026-08-02T14:20:00Z"
            updated_at: "2026-08-02T14:20:00Z"
    WebhookEvent:
      type: string
      enum: [operation.completed, operation.failed, lead_list.completed, lead_list.failed, enrichment.completed, enrichment.failed, balance.updated]
    CreateWebhookEndpointInput:
      type: object
      additionalProperties: false
      required: [name, url, events]
      properties:
        name: { type: string, minLength: 1, maxLength: 160 }
        url: { type: string, format: uri, maxLength: 2048, pattern: "^https://" }
        events:
          type: array
          minItems: 1
          uniqueItems: true
          items: { $ref: "#/components/schemas/WebhookEvent" }
        description: { type: string, maxLength: 500 }
      example:
        name: Integração comercial
        url: https://integracao.example.com/webhooks/tdfacil
        events: [operation.completed, operation.failed, lead_list.completed]
        description: Recebe eventos do fluxo de prospecção
    UpdateWebhookEndpointInput:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        name: { type: string, minLength: 1, maxLength: 160 }
        url: { type: string, format: uri, maxLength: 2048, pattern: "^https://" }
        events:
          type: array
          minItems: 1
          uniqueItems: true
          items: { $ref: "#/components/schemas/WebhookEvent" }
        description: { type: [string, "null"], maxLength: 500 }
        status: { type: string, enum: [active, disabled] }
      example:
        name: Integração comercial principal
        events: [operation.completed, operation.failed, lead_list.completed, enrichment.completed]
        status: active
    WebhookEndpoint:
      type: object
      required: [id, name, url, events, description, status, created_at, updated_at]
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        url: { type: string, format: uri }
        events: { type: array, items: { $ref: "#/components/schemas/WebhookEvent" } }
        description: { type: [string, "null"] }
        status: { type: string }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
      example:
        id: 40000000-0000-4000-8000-000000000001
        name: Integração comercial
        url: https://integracao.example.com/webhooks/tdfacil
        events: [operation.completed, operation.failed, lead_list.completed]
        description: Recebe eventos do fluxo de prospecção
        status: active
        created_at: "2026-08-02T15:00:00Z"
        updated_at: "2026-08-02T15:00:00Z"
    CreatedWebhookEndpoint:
      allOf:
        - $ref: "#/components/schemas/WebhookEndpoint"
        - type: object
          required: [signing_secret]
          properties:
            signing_secret: { type: string, pattern: "^whsec_[A-Za-z0-9_-]{43}$", readOnly: true }
      example:
        id: 40000000-0000-4000-8000-000000000001
        name: Integração comercial
        url: https://integracao.example.com/webhooks/tdfacil
        events: [operation.completed, operation.failed, lead_list.completed]
        description: Recebe eventos do fluxo de prospecção
        status: active
        created_at: "2026-08-02T15:00:00Z"
        updated_at: "2026-08-02T15:00:00Z"
        signing_secret: whsec_EXAMPLE_ONLY_0123456789abcdefghijklmnop
    WebhookEndpointPage:
      type: object
      required: [data, meta]
      properties:
        data: { type: array, items: { $ref: "#/components/schemas/WebhookEndpoint" } }
        meta: { $ref: "#/components/schemas/PageMeta" }
      example:
        data:
          - id: 40000000-0000-4000-8000-000000000001
            name: Integração comercial
            url: https://integracao.example.com/webhooks/tdfacil
            events: [operation.completed, operation.failed, lead_list.completed]
            description: Recebe eventos do fluxo de prospecção
            status: active
            created_at: "2026-08-02T15:00:00Z"
            updated_at: "2026-08-02T15:00:00Z"
        meta:
          next_cursor: null
    Problem:
      type: object
      required: [type, title, status, detail, instance, code, request_id]
      properties:
        type: { type: string, format: uri-reference }
        title: { type: string }
        status: { type: integer, minimum: 400, maximum: 599 }
        detail: { type: string }
        instance: { type: string }
        code: { type: string }
        request_id: { type: string }
        errors:
          type: array
          items:
            type: object
            required: [path, message]
            properties:
              path: { type: string }
              message: { type: string }
        metadata: { type: object, additionalProperties: true }
