openapi: 3.1.0
info:
  title: API de CRM de Whapi
  version: 1.0.0
  description: |
    Lee y actualiza los **leads, contactos y empresas** de tu cuenta de Whapi, y consulta el historial de cada lead.

    - **Guías, autenticación y errores:** [whapi.chat/desarrolladores](https://www.whapi.chat/desarrolladores)
    - **Autenticación:** `Authorization: Bearer <clave de API>`. Las claves se crean en *Configuración de la empresa → Claves de API*.
    - **Búsqueda por teléfono:** las rutas `by-phone` aceptan el número con código de país, solo dígitos (`51987654321`).
    - **Sin webhooks propios:** los cambios hechos por esta API **no disparan** `lead.updated`, para que tu sistema no reciba de vuelta sus propios cambios.

    > **"Test Request" ejecuta llamadas reales** con la clave que ingreses: un `PATCH` modifica datos de tu cuenta.
  contact:
    name: Soporte de Whapi
    url: https://www.whapi.chat/contacto
servers:
  - url: https://api.whapi.chat/openapi
    description: Producción
security:
  - apiKey: []
tags:
  - name: Leads
    description: Oportunidades del embudo, con sus contactos, empresa y campos personalizados.
  - name: Contactos
    description: Personas con las que conversas, con sus canales y campos personalizados.
  - name: Empresas
    description: Empresas vinculadas a leads y contactos.
  - name: Eventos del lead
    description: |
      Historial de un lead: mensajes, notas, tareas, citas, cotizaciones, ventas, llamadas y cambios de campos. Solo lectura.

      Los eventos vienen del más reciente al más antiguo, de a 50 como máximo.
paths:
  /leads/{leadId}:
    parameters:
      - $ref: "#/components/parameters/LeadId"
    get:
      operationId: getLead
      tags: [Leads]
      summary: Ver lead
      responses:
        "200":
          $ref: "#/components/responses/LeadOk"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/LeadNotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
    patch:
      operationId: updateLead
      tags: [Leads]
      summary: Actualizar lead
      description: Actualiza solo los campos enviados. Responde el lead completo, ya actualizado.
      requestBody:
        $ref: "#/components/requestBodies/LeadUpdate"
      responses:
        "200":
          $ref: "#/components/responses/LeadOk"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/LeadNotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
  /leads/by-phone/{number}:
    parameters:
      - $ref: "#/components/parameters/PhoneNumber"
    get:
      operationId: getLeadByPhone
      tags: [Leads]
      summary: Ver lead por teléfono
      description: Busca el contacto con ese teléfono y devuelve su lead activo. Si el contacto tiene varios leads activos, responde `409` con sus ids en `leadIds`.
      responses:
        "200":
          $ref: "#/components/responses/LeadOk"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/LeadByPhoneNotFound"
        "409":
          $ref: "#/components/responses/MultipleLeads"
        "429":
          $ref: "#/components/responses/RateLimited"
    patch:
      operationId: updateLeadByPhone
      tags: [Leads]
      summary: Actualizar lead por teléfono
      description: Igual que `PATCH /leads/{leadId}`, ubicando el lead por el teléfono de su contacto.
      requestBody:
        $ref: "#/components/requestBodies/LeadUpdate"
      responses:
        "200":
          $ref: "#/components/responses/LeadOk"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/LeadByPhoneNotFound"
        "409":
          $ref: "#/components/responses/MultipleLeads"
        "429":
          $ref: "#/components/responses/RateLimited"
  /contacts/{contactId}:
    parameters:
      - name: contactId
        in: path
        required: true
        schema:
          type: integer
          minimum: 1
    get:
      operationId: getContact
      tags: [Contactos]
      summary: Ver contacto
      responses:
        "200":
          $ref: "#/components/responses/ContactOk"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/ContactNotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
    patch:
      operationId: updateContact
      tags: [Contactos]
      summary: Actualizar contacto
      description: Actualiza solo los campos enviados. Responde el contacto completo, ya actualizado.
      requestBody:
        $ref: "#/components/requestBodies/ContactUpdate"
      responses:
        "200":
          $ref: "#/components/responses/ContactOk"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/ContactNotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
  /contacts/by-phone/{number}:
    parameters:
      - $ref: "#/components/parameters/PhoneNumber"
    get:
      operationId: getContactByPhone
      tags: [Contactos]
      summary: Ver contacto por teléfono
      description: Si hay varios contactos con ese teléfono, responde `409` con sus ids en `contactIds`.
      responses:
        "200":
          $ref: "#/components/responses/ContactOk"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/ContactNotFound"
        "409":
          $ref: "#/components/responses/MultipleContacts"
        "429":
          $ref: "#/components/responses/RateLimited"
    patch:
      operationId: updateContactByPhone
      tags: [Contactos]
      summary: Actualizar contacto por teléfono
      requestBody:
        $ref: "#/components/requestBodies/ContactUpdate"
      responses:
        "200":
          $ref: "#/components/responses/ContactOk"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/ContactNotFound"
        "409":
          $ref: "#/components/responses/MultipleContacts"
        "429":
          $ref: "#/components/responses/RateLimited"
  /businesses/{businessId}:
    parameters:
      - name: businessId
        in: path
        required: true
        schema:
          type: integer
          minimum: 1
    get:
      operationId: getBusiness
      tags: [Empresas]
      summary: Ver empresa
      responses:
        "200":
          $ref: "#/components/responses/BusinessOk"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/BusinessNotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
    patch:
      operationId: updateBusiness
      tags: [Empresas]
      summary: Actualizar empresa
      description: Actualiza solo los campos enviados. Responde la empresa completa, ya actualizada.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/BusinessUpdate"
            example:
              ruc: "20123456789"
              industry: Retail
              averageTicket: 1500
              customFields:
                - name: Rubro
                  value: Salud
      responses:
        "200":
          $ref: "#/components/responses/BusinessOk"
        "400":
          description: |
            `INVALID_REQUEST` (campo desconocido o mal tipado), `ERR_BUSINESS_ALREADY_EXISTS` (ya hay una empresa con ese nombre), `ERR_BUSINESS_PHONE_INVALID_FORMAT` / `ERR_BUSINESS_PHONE_INVALID_LENGTH` (el teléfono debe tener de 6 a 15 dígitos).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error: "Unknown field: position"
                code: INVALID_REQUEST
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/BusinessNotFound"
        "409":
          description: "`ERR_BUSINESS_RUC_ALREADY_EXISTS`: otra empresa del mismo país ya tiene ese RUC o número fiscal."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          $ref: "#/components/responses/RateLimited"
  /leads/{leadId}/events:
    parameters:
      - $ref: "#/components/parameters/LeadId"
    get:
      operationId: listLeadEvents
      tags: [Eventos del lead]
      summary: Listar eventos del lead
      parameters:
        - $ref: "#/components/parameters/PageNumber"
        - $ref: "#/components/parameters/PageSize"
        - $ref: "#/components/parameters/EventType"
        - $ref: "#/components/parameters/EventAction"
        - $ref: "#/components/parameters/EventCategory"
      responses:
        "200":
          $ref: "#/components/responses/LeadEventListOk"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/LeadNotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
  /leads/by-phone/{number}/events:
    parameters:
      - $ref: "#/components/parameters/PhoneNumber"
    get:
      operationId: listLeadEventsByPhone
      tags: [Eventos del lead]
      summary: Listar eventos del lead por teléfono
      parameters:
        - $ref: "#/components/parameters/PageNumber"
        - $ref: "#/components/parameters/PageSize"
        - $ref: "#/components/parameters/EventType"
        - $ref: "#/components/parameters/EventAction"
        - $ref: "#/components/parameters/EventCategory"
      responses:
        "200":
          $ref: "#/components/responses/LeadEventListOk"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/LeadByPhoneNotFound"
        "409":
          $ref: "#/components/responses/MultipleLeads"
        "429":
          $ref: "#/components/responses/RateLimited"
  /lead-events/{leadEventId}:
    parameters:
      - name: leadEventId
        in: path
        required: true
        schema:
          type: integer
          minimum: 1
    get:
      operationId: getLeadEvent
      tags: [Eventos del lead]
      summary: Ver evento
      responses:
        "200":
          description: El evento.
          headers:
            X-Whapi-Api-Key-Expires-At:
              $ref: "#/components/headers/ApiKeyExpiresAt"
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required: [leadEvent]
                properties:
                  leadEvent:
                    $ref: "#/components/schemas/LeadEvent"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: "`ERR_NO_LEAD_EVENT_FOUND`"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          $ref: "#/components/responses/RateLimited"
webhooks:
  lead.created:
    post:
      operationId: webhookLeadCreated
      summary: Lead creado
      description: |
        Se creó un lead, a mano o automáticamente (por ejemplo, al llegar el primer mensaje de un contacto nuevo). `data` tiene la misma forma que la respuesta de `GET /leads/{leadId}`, más `source`.

        Cómo verificar la firma y qué responder: [guía de webhooks](https://www.whapi.chat/desarrolladores/webhooks).
      parameters:
        - $ref: "#/components/parameters/WebhookEventId"
        - $ref: "#/components/parameters/WebhookEventType"
        - $ref: "#/components/parameters/WebhookTimestamp"
        - $ref: "#/components/parameters/WebhookSignature"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LeadCreatedEvent"
      responses:
        "200":
          $ref: "#/components/responses/WebhookAck"
  lead.updated:
    post:
      operationId: webhookLeadUpdated
      summary: Lead actualizado
      description: |
        Cambió un lead, sus contactos, sus canales o sus campos personalizados, desde Whapi. Los cambios hechos por esta API no lo disparan.

        `data` tiene la misma forma que la respuesta de `GET /leads/{leadId}`.
      parameters:
        - $ref: "#/components/parameters/WebhookEventId"
        - $ref: "#/components/parameters/WebhookEventType"
        - $ref: "#/components/parameters/WebhookTimestamp"
        - $ref: "#/components/parameters/WebhookSignature"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LeadUpdatedEvent"
      responses:
        "200":
          $ref: "#/components/responses/WebhookAck"
components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      bearerFormat: whk_…
      description: Clave de API de la empresa. Se crea en *Configuración de la empresa → Claves de API* y se muestra una sola vez.
  headers:
    ApiKeyExpiresAt:
      description: Solo si la clave fue regenerada y está en su período de gracia. Fecha (ISO 8601) en que deja de funcionar.
      schema:
        type: string
        format: date-time
    RetryAfter:
      description: Segundos a esperar antes de reintentar.
      schema:
        type: integer
  parameters:
    LeadId:
      name: leadId
      in: path
      required: true
      schema:
        type: integer
        minimum: 1
    PhoneNumber:
      name: number
      in: path
      required: true
      description: Teléfono con código de país, solo dígitos.
      schema:
        type: string
      example: "51987654321"
    PageNumber:
      name: pageNumber
      in: query
      schema:
        type: integer
        minimum: 1
        default: 1
    PageSize:
      name: pageSize
      in: query
      description: Eventos por página. Máximo 50.
      schema:
        type: integer
        minimum: 1
        maximum: 50
        default: 50
    EventType:
      name: type
      in: query
      description: Filtra por tipo de evento.
      schema:
        type: string
        enum: [message, note, task, schedule, quote, sell, tag, call, payment_link, snapshot]
    EventAction:
      name: action
      in: query
      schema:
        type: string
        enum: [create, update, delete]
    EventCategory:
      name: category
      in: query
      description: "`entity`: algo creado en el lead (mensaje, nota, tarea…). `snapshot`: cambio de un campo. `log`: registro del sistema."
      schema:
        type: string
        enum: [entity, snapshot, log]
    WebhookEventId:
      name: X-Whapi-Event-Id
      in: header
      required: true
      description: Id único del evento (el mismo que `id` en el body). Úsalo para descartar reintentos repetidos.
      schema:
        type: string
        format: uuid
    WebhookEventType:
      name: X-Whapi-Event-Type
      in: header
      required: true
      description: Tipo de evento (el mismo que `type` en el body).
      schema:
        type: string
    WebhookTimestamp:
      name: X-Whapi-Timestamp
      in: header
      required: true
      description: Momento del envío, en segundos Unix. Forma parte de la firma.
      schema:
        type: string
    WebhookSignature:
      name: X-Whapi-Signature
      in: header
      required: true
      description: '`sha256=` + HMAC-SHA256 en hexadecimal de `"{X-Whapi-Timestamp}.{body crudo}"`, con el secreto de firma de tu endpoint.'
      schema:
        type: string
  requestBodies:
    LeadUpdate:
      required: true
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/LeadUpdate"
          examples:
            stage:
              summary: Mover de etapa y cambiar el monto
              value:
                stageId: 12
                price: 1500
            customFields:
              summary: Completar campos personalizados
              value:
                priority: high
                customFields:
                  - name: Origen
                    value: Feria 2026
    ContactUpdate:
      required: true
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ContactUpdate"
          example:
            customName: Ana Pérez
            email: ana@correo.com
            docType: national_id
            countryIdNumber: "45678912"
  responses:
    LeadOk:
      description: El lead, con sus contactos, empresa, responsable y la definición de los campos personalizados.
      headers:
        X-Whapi-Api-Key-Expires-At:
          $ref: "#/components/headers/ApiKeyExpiresAt"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/LeadData"
          example:
            lead:
              id: 9
              name: Ana Pérez
              stageId: 12
              userId: 4
              status: open
              price: "1500.00"
              businessId: 7
              createdAt: "2026-10-01T14:00:00.000Z"
              updatedAt: "2026-10-14T15:04:05.000Z"
              stage:
                id: 12
                name: Propuesta enviada
                color: "#22c55e"
                position: 3
                funnelId: 2
              customFields:
                - id: 31
                  name: Origen
                  value: Feria 2026
                  companyCustomFieldId: 5
            contacts:
              - id: 18
                name: Ana Pérez
                customName: Ana Pérez
                number: "51987654321"
                numberLid: null
                email: ana@correo.com
                birthday: null
                countryCode: PE
                provinceCode: null
                countryIdNumber: "45678912"
                docType: national_id
                profilePicUrl: null
                app: waba
                igUsername: null
                active: true
                createdAt: "2026-10-01T14:00:00.000Z"
                updatedAt: "2026-10-14T15:04:05.000Z"
                link:
                  active: true
                  main: true
                customFields: []
                channels: []
            business: null
            user:
              id: 4
              name: Carlos Mendoza
              email: carlos@miempresa.com
            companyCustomFields:
              - id: 5
                name: Origen
                type: text
                options: null
                icon: null
                required: false
                model: lead
                position: 1
                description: null
    ContactOk:
      description: El contacto y la definición de los campos personalizados de contactos.
      headers:
        X-Whapi-Api-Key-Expires-At:
          $ref: "#/components/headers/ApiKeyExpiresAt"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ContactData"
    BusinessOk:
      description: La empresa.
      headers:
        X-Whapi-Api-Key-Expires-At:
          $ref: "#/components/headers/ApiKeyExpiresAt"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Business"
          example:
            id: 7
            name: Acme SAC
            ruc: "20123456789"
            email: contacto@acme.pe
            phone: "014567890"
            address: Av. Larco 123, Miraflores
            industry: Retail
            anniversary: null
            averageTicket: 1500
            firstLegalRepresentative: null
            firstLegalRepresentativeSince: null
            countryCode: PE
            customFields:
              - id: 50
                name: Rubro
                value: Salud
                companyCustomFieldId: 1
            createdAt: "2026-09-01T10:00:00.000Z"
            updatedAt: "2026-10-14T15:04:05.000Z"
    LeadEventListOk:
      description: Una página de eventos.
      headers:
        X-Whapi-Api-Key-Expires-At:
          $ref: "#/components/headers/ApiKeyExpiresAt"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/LeadEventList"
    InvalidRequest:
      description: "`INVALID_REQUEST`: parámetro o body inválido (campo desconocido, tipo incorrecto, valor fuera de los permitidos). `error` explica cuál."
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: "status must be one of: pending, open, archived, closed"
            code: INVALID_REQUEST
    Unauthorized:
      description: |
        Clave ausente o no válida.

        | `code` | Causa |
        |---|---|
        | `AUTH_HEADER_MISSING` | Falta `Authorization: Bearer <clave>`. |
        | `API_KEY_INVALID` | La clave no existe. |
        | `API_KEY_REVOKED` | La clave fue revocada. |
        | `API_KEY_EXPIRED` | La clave fue regenerada y terminó su período de gracia (`expiredAt`). |
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: Company not found for the provided API key
            code: API_KEY_INVALID
    LeadNotFound:
      description: "`ERR_NO_LEAD_FOUND`"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: ERR_NO_LEAD_FOUND
            code: ERR_NO_LEAD_FOUND
    LeadByPhoneNotFound:
      description: "`ERR_NO_CONTACT_FOUND_FOR_PHONE`: no hay un contacto con ese teléfono. `ERR_NO_LEAD_FOUND_FOR_CONTACT`: el contacto no tiene leads activos."
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    ContactNotFound:
      description: "`ERR_NO_CONTACT_FOUND` o, por teléfono, `ERR_NO_CONTACT_FOUND_FOR_PHONE`."
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    BusinessNotFound:
      description: "`ERR_BUSINESS_NOT_FOUND`"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    MultipleLeads:
      description: "`CONFLICT`: el contacto tiene varios leads activos. Usa uno de los ids de `leadIds` con la ruta por id."
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: Multiple active leads found for this phone number. Use lead id instead.
            code: CONFLICT
            leadIds: [9, 14]
    MultipleContacts:
      description: "`CONFLICT`: hay varios contactos con ese teléfono. Usa uno de los ids de `contactIds` con la ruta por id."
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: Multiple contacts found for this phone number. Use contact id instead.
            code: CONFLICT
            contactIds: [18, 22]
    RateLimited:
      description: "`RATE_LIMITED`: superaste el límite de requests por minuto de tu cuenta."
      headers:
        Retry-After:
          $ref: "#/components/headers/RetryAfter"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: Too many requests
            code: RATE_LIMITED
            limitPerMinute: 300
    WebhookAck:
      description: Responde cualquier 2xx en menos de 15 segundos. Un `408`, `429`, `5xx`, un timeout o un error de red se reintentan (hasta 5 intentos en total, con esperas crecientes desde 1 minuto). Cualquier otro `4xx` se da por fallido y no se reintenta.
  schemas:
    Error:
      type: object
      required: [error, code]
      description: Forma de todos los errores. Algunos códigos agregan campos (`leadIds`, `contactIds`, `expiredAt`, `limitPerMinute`).
      properties:
        error:
          type: string
          description: Mensaje legible. Puede cambiar; para decidir, usa `code`.
        code:
          type: string
          description: Código estable del error.
      additionalProperties: true
    NullableDateTime:
      type: [string, "null"]
      format: date-time
    CustomFieldValue:
      type: object
      additionalProperties: false
      required: [id, name, value, companyCustomFieldId]
      properties:
        id:
          type: integer
        name:
          type: [string, "null"]
        value:
          type: [string, "null"]
          description: Valor guardado. En campos de selección es una lista JSON de claves de opción.
        companyCustomFieldId:
          type: [integer, "null"]
          description: Definición del campo (ver `companyCustomFields`).
    CustomFieldInput:
      type: object
      required: [name, value]
      properties:
        name:
          type: string
          description: Nombre del campo, tal como está definido en Whapi.
        value:
          type: string
          description: En campos de selección, el texto o la clave de la opción.
        companyCustomFieldId:
          type: integer
          description: Alternativa a `name` para identificar el campo.
    CompanyCustomField:
      type: object
      additionalProperties: false
      required: [id, name, type, options, icon, required, model, position, description]
      description: Definición de un campo personalizado.
      properties:
        id:
          type: integer
        name:
          type: [string, "null"]
        type:
          type: [string, "null"]
          description: '`text`, `number`, `date`, `select`, `multiselect`…'
        options:
          description: Opciones de los campos de selección.
        icon:
          type: [string, "null"]
        required:
          type: boolean
        model:
          type: [string, "null"]
          description: '`lead`, `contact` o `business`.'
        position:
          type: [integer, "null"]
        description:
          type: [string, "null"]
    Stage:
      type: object
      additionalProperties: false
      required: [id, name, color, position, funnelId]
      properties:
        id:
          type: integer
        name:
          type: [string, "null"]
        color:
          type: [string, "null"]
        position:
          type: [integer, "null"]
        funnelId:
          type: [integer, "null"]
    Lead:
      type: object
      additionalProperties: false
      required: [id, name, stageId, userId, status, price, businessId, createdAt, updatedAt, stage, customFields]
      properties:
        id:
          type: integer
        name:
          type: [string, "null"]
        stageId:
          type: [integer, "null"]
        userId:
          type: [integer, "null"]
          description: Usuario responsable.
        status:
          type: [string, "null"]
          enum: [pending, open, archived, closed, null]
        price:
          type: [string, number]
          description: Monto del lead. Llega como texto con dos decimales (`"1500.00"`), o `0`.
        businessId:
          type: [integer, "null"]
        createdAt:
          $ref: "#/components/schemas/NullableDateTime"
        updatedAt:
          $ref: "#/components/schemas/NullableDateTime"
        stage:
          oneOf:
            - $ref: "#/components/schemas/Stage"
            - type: "null"
        customFields:
          type: array
          items:
            $ref: "#/components/schemas/CustomFieldValue"
    ChannelAccount:
      type: object
      required: [type, id]
      description: Cuenta de Whapi por la que se habla con el contacto. Los campos dependen de `type`.
      properties:
        type:
          type: string
          enum: [whatsapp, waba, facebook, instagram, tiktok]
        id:
          type: integer
      additionalProperties: true
    ContactChannel:
      type: object
      additionalProperties: false
      required: [id, channel, platformId, altPlatformId, metadata, account, createdAt, updatedAt]
      properties:
        id:
          type: integer
        channel:
          type: [string, "null"]
          description: '`whatsapp`, `waba`, `facebook`, `instagram`, `tiktok`…'
        platformId:
          type: [string, "null"]
          description: Id del contacto en ese canal.
        altPlatformId:
          type: [string, "null"]
        metadata:
          type: [object, "null"]
        account:
          oneOf:
            - $ref: "#/components/schemas/ChannelAccount"
            - type: "null"
        createdAt:
          $ref: "#/components/schemas/NullableDateTime"
        updatedAt:
          $ref: "#/components/schemas/NullableDateTime"
    Contact:
      type: object
      additionalProperties: false
      required: [id, name, customName, number, numberLid, email, birthday, countryCode, provinceCode, countryIdNumber, docType, profilePicUrl, app, igUsername, active, createdAt, updatedAt, link, customFields, channels]
      properties:
        id:
          type: integer
        name:
          type: [string, "null"]
          description: Nombre a mostrar (`customName` si existe).
        customName:
          type: [string, "null"]
          description: Nombre puesto en Whapi.
        number:
          type: [string, "null"]
          description: Teléfono con código de país, solo dígitos.
        numberLid:
          type: [string, "null"]
        email:
          type: [string, "null"]
        birthday:
          type: [string, "null"]
        countryCode:
          type: [string, "null"]
        provinceCode:
          type: [string, "null"]
        countryIdNumber:
          type: [string, "null"]
          description: Número de documento.
        docType:
          type: [string, "null"]
          enum: [national_id, residence_id, passport, other, null]
        profilePicUrl:
          type: [string, "null"]
        app:
          type: [string, "null"]
          description: Canal principal del contacto.
        igUsername:
          type: [string, "null"]
        active:
          type: [boolean, "null"]
        createdAt:
          $ref: "#/components/schemas/NullableDateTime"
        updatedAt:
          $ref: "#/components/schemas/NullableDateTime"
        link:
          type: object
          additionalProperties: false
          required: [active, main]
          description: 'Relación con el lead (en respuestas de contacto suelto, `active: true` y `main: false`).'
          properties:
            active:
              type: boolean
            main:
              type: boolean
              description: Contacto principal del lead.
        customFields:
          type: array
          items:
            $ref: "#/components/schemas/CustomFieldValue"
        channels:
          type: array
          items:
            $ref: "#/components/schemas/ContactChannel"
    BusinessFields:
      type: object
      required: [id, name, ruc, email, phone, address, industry, anniversary, averageTicket, firstLegalRepresentative, firstLegalRepresentativeSince, countryCode, customFields]
      properties:
        id:
          type: integer
        name:
          type: [string, "null"]
        ruc:
          type: [string, "null"]
          description: RUC o número fiscal.
        email:
          type: [string, "null"]
        phone:
          type: [string, "null"]
        address:
          type: [string, "null"]
        industry:
          type: [string, "null"]
        anniversary:
          type: [string, "null"]
          format: date
        averageTicket:
          type: [number, "null"]
        firstLegalRepresentative:
          type: [string, "null"]
        firstLegalRepresentativeSince:
          type: [string, "null"]
          format: date
        countryCode:
          type: [string, "null"]
        customFields:
          type: array
          items:
            $ref: "#/components/schemas/CustomFieldValue"
    LeadBusiness:
      description: Empresa del lead (sin fechas).
      allOf:
        - $ref: "#/components/schemas/BusinessFields"
      unevaluatedProperties: false
    Business:
      allOf:
        - $ref: "#/components/schemas/BusinessFields"
        - type: object
          required: [createdAt, updatedAt]
          properties:
            createdAt:
              $ref: "#/components/schemas/NullableDateTime"
            updatedAt:
              $ref: "#/components/schemas/NullableDateTime"
      unevaluatedProperties: false
    User:
      type: object
      additionalProperties: false
      required: [id, name, email]
      properties:
        id:
          type: integer
        name:
          type: [string, "null"]
        email:
          type: [string, "null"]
    LeadData:
      type: object
      additionalProperties: false
      required: [lead, contacts, business, user, companyCustomFields]
      properties:
        lead:
          $ref: "#/components/schemas/Lead"
        contacts:
          type: array
          items:
            $ref: "#/components/schemas/Contact"
        business:
          oneOf:
            - $ref: "#/components/schemas/LeadBusiness"
            - type: "null"
        user:
          description: Usuario responsable del lead.
          oneOf:
            - $ref: "#/components/schemas/User"
            - type: "null"
        companyCustomFields:
          type: array
          description: Definición de los campos personalizados que aparecen en el lead, sus contactos y su empresa.
          items:
            $ref: "#/components/schemas/CompanyCustomField"
        source:
          type: string
          enum: [manual, inbound_message, ticket]
          description: Solo en webhooks. Cómo se creó el lead.
        test:
          type: boolean
          description: Solo en el evento de prueba enviado desde Whapi.
    ContactData:
      type: object
      additionalProperties: false
      required: [contact, companyCustomFields]
      properties:
        contact:
          $ref: "#/components/schemas/Contact"
        companyCustomFields:
          type: array
          items:
            $ref: "#/components/schemas/CompanyCustomField"
    LeadUpdate:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        name:
          type: string
        price:
          type: number
        stageId:
          type: [integer, "null"]
          description: Etapa del embudo. Tiene que existir.
        status:
          type: string
          enum: [pending, open, archived, closed]
        priority:
          type: string
          enum: [low, medium, high]
        businessId:
          type: [integer, "null"]
        customFields:
          type: array
          description: Solo campos ya definidos para leads. Los que no se envían no cambian.
          items:
            $ref: "#/components/schemas/CustomFieldInput"
    ContactUpdate:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        name:
          type: string
        customName:
          type: string
        number:
          type: string
          description: Teléfono con código de país, solo dígitos.
        email:
          type: string
        birthday:
          type: [string, "null"]
          format: date
        countryCode:
          type: [string, "null"]
        provinceCode:
          type: [string, "null"]
        countryIdNumber:
          type: [string, "null"]
        docType:
          type: [string, "null"]
          enum: [national_id, residence_id, passport, other, null]
        customFields:
          type: array
          description: Solo campos ya definidos para contactos.
          items:
            $ref: "#/components/schemas/CustomFieldInput"
    BusinessUpdate:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        name:
          type: string
          minLength: 1
        ruc:
          type: [string, "null"]
        email:
          type: [string, "null"]
        phone:
          type: [string, "null"]
          description: De 6 a 15 dígitos.
        address:
          type: [string, "null"]
        industry:
          type: [string, "null"]
        anniversary:
          type: [string, "null"]
          format: date
        firstLegalRepresentative:
          type: [string, "null"]
        firstLegalRepresentativeSince:
          type: [string, "null"]
          format: date
        countryCode:
          type: string
          pattern: "^[A-Za-z]{2}$"
          description: Código ISO de dos letras.
        averageTicket:
          type: [number, "null"]
          minimum: 0
        customFields:
          type: array
          description: Solo campos ya definidos para empresas.
          items:
            $ref: "#/components/schemas/CustomFieldInput"
    LeadEvent:
      type: object
      additionalProperties: false
      required: [id, leadId, action, type, category, data, summary, messageId, ticketNoteId, taskId, scheduleId, quoteId, message, note, task, schedule, quote, call, userId, user, aiEvent, autoEvent, createdAt, updatedAt]
      properties:
        id:
          type: integer
        leadId:
          type: integer
        action:
          type: string
          description: '`create`, `update` o `delete`.'
        type:
          type: string
          description: '`message`, `note`, `task`, `schedule`, `quote`, `sell`, `tag`, `call`, `payment_link`, `snapshot`…'
        category:
          type: string
          description: '`entity`, `snapshot` o `log`.'
        data:
          type: object
          description: Detalle del evento. En `snapshot`, el campo cambiado con `prevValue` y `newValue`.
        summary:
          type: string
          description: Resumen legible del evento.
        messageId:
          type: [integer, "null"]
        ticketNoteId:
          type: [integer, "null"]
        taskId:
          type: [integer, "null"]
        scheduleId:
          type: [integer, "null"]
        quoteId:
          type: [integer, "null"]
        message:
          oneOf:
            - type: object
              additionalProperties: false
              required: [id, body, fromMe, mediaType, app, isDeleted, createdAt]
              properties:
                id:
                  type: integer
                body:
                  type: [string, "null"]
                fromMe:
                  type: boolean
                mediaType:
                  type: [string, "null"]
                app:
                  type: [string, "null"]
                isDeleted:
                  type: boolean
                createdAt:
                  $ref: "#/components/schemas/NullableDateTime"
            - type: "null"
        note:
          oneOf:
            - type: object
              additionalProperties: false
              required: [id, note]
              properties:
                id:
                  type: integer
                note:
                  type: [string, "null"]
            - type: "null"
        task:
          oneOf:
            - type: object
              additionalProperties: false
              required: [id, title, status, dueAt, priority]
              properties:
                id:
                  type: integer
                title:
                  type: [string, "null"]
                status:
                  type: [string, "null"]
                dueAt:
                  $ref: "#/components/schemas/NullableDateTime"
                priority:
                  type: [string, "null"]
            - type: "null"
        schedule:
          oneOf:
            - type: object
              additionalProperties: false
              required: [id, title, startDate, endDate, sendAt, status, type]
              properties:
                id:
                  type: integer
                title:
                  type: [string, "null"]
                startDate:
                  $ref: "#/components/schemas/NullableDateTime"
                endDate:
                  $ref: "#/components/schemas/NullableDateTime"
                sendAt:
                  $ref: "#/components/schemas/NullableDateTime"
                status:
                  type: [string, "null"]
                type:
                  type: [string, "null"]
            - type: "null"
        quote:
          oneOf:
            - type: object
              additionalProperties: false
              required: [id, title, status, subStatus, total, currency]
              properties:
                id:
                  type: integer
                title:
                  type: [string, "null"]
                status:
                  type: [string, "null"]
                subStatus:
                  type: [string, "null"]
                total:
                  type: [number, "null"]
                currency:
                  type: [string, "null"]
            - type: "null"
        call:
          oneOf:
            - type: object
              additionalProperties: false
              required: [id, direction, status, duration, fromNumber, toNumber]
              properties:
                id:
                  type: integer
                direction:
                  type: [string, "null"]
                status:
                  type: [string, "null"]
                duration:
                  type: [integer, "null"]
                  description: Segundos.
                fromNumber:
                  type: [string, "null"]
                toNumber:
                  type: [string, "null"]
            - type: "null"
        userId:
          type: [integer, "null"]
        user:
          oneOf:
            - type: object
              additionalProperties: false
              required: [id, name]
              properties:
                id:
                  type: integer
                name:
                  type: string
            - type: "null"
        aiEvent:
          type: boolean
          description: Lo hizo WhapiBot.
        autoEvent:
          type: boolean
          description: Lo hizo una automatización.
        createdAt:
          $ref: "#/components/schemas/NullableDateTime"
        updatedAt:
          $ref: "#/components/schemas/NullableDateTime"
    LeadEventList:
      type: object
      additionalProperties: false
      required: [leadEvents, count, hasMore, pageNumber, pageSize]
      properties:
        leadEvents:
          type: array
          items:
            $ref: "#/components/schemas/LeadEvent"
        count:
          type: integer
          description: Total de eventos que cumplen los filtros.
        hasMore:
          type: boolean
        pageNumber:
          type: integer
        pageSize:
          type: integer
    WebhookEnvelope:
      type: object
      required: [apiVersion, id, type, createdAt, data]
      properties:
        apiVersion:
          type: string
          const: "2026-06-15"
          description: Versión del formato de los eventos.
        id:
          type: string
          format: uuid
          description: Id único del evento. Los reintentos repiten el mismo id.
        type:
          type: string
        createdAt:
          type: string
          format: date-time
        data:
          type: object
    LeadCreatedEvent:
      allOf:
        - $ref: "#/components/schemas/WebhookEnvelope"
        - type: object
          properties:
            type:
              const: lead.created
            data:
              $ref: "#/components/schemas/LeadData"
    LeadUpdatedEvent:
      allOf:
        - $ref: "#/components/schemas/WebhookEnvelope"
        - type: object
          properties:
            type:
              const: lead.updated
            data:
              $ref: "#/components/schemas/LeadData"
