openapi: 3.0.3
info:
  title: MasLeads API
  description: |
    API pública de MasLeads para obtener emails y teléfonos profesionales verificados.

    ## Planes con Acceso

    Esta API está disponible exclusivamente para los siguientes planes:
    - **Professional**
    - **Elite**
    - **Custom**

    Si tienes un plan Basic o Scale, contacta c contacta con soporte para evaluar tus opciones.

    ## Límites por Defecto

    Los siguientes son valores por defecto. Pueden variar según tu plan contratado.

    | Límite | Valor por defecto |
    |--------|-------------------|
    | API Keys por empresa | 10 |
    | Requests por minuto | 300 |
    | Leads por job | 100 |
    | Jobs concurrentes | 10 |

    ## Cómo Funciona

    1. **Crear Job**: `POST /enrich/leads` - Envía tus leads
    2. **Polling o Webhook**: Espera a que el job complete
    3. **Obtener Resultados**: `GET /jobs/{job_id}/results`
  version: 1.0.0
  contact:
    name: MasLeads
  license:
    name: Proprietary
  x-logo:
    url: /assets/images/logo.svg
    altText: MasLeads

servers:
  - url: https://api.masleads.com/api/v1
    description: Production

tags:
  - name: API Keys
    description: |
      Gestión de API Keys para autenticación.

      Puedes crear, listar y revocar API Keys programáticamente.
      Máximo 10 keys activas por empresa.
  - name: Jobs
    description: |
      Consultar estado y resultados de jobs.

      Usa polling (`GET /jobs/{job_id}`) o webhooks para saber cuándo
      un job ha completado.
  - name: Webhooks
    description: |
      Configuración de webhooks para notificaciones.

      Configura un webhook por defecto para recibir notificaciones
      automáticas cuando tus jobs se completen.
  - name: Usage
    description: Consultar créditos disponibles y uso

security:
  - ApiKeyAuth: []

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: |
        API Key de autenticación. Formato: `sk_live_xxxxx...`

        Obtén tu API Key en: **Perfil > Configuración > API Keys** o bien vía API en el endpoint correspondiente

        Ejemplo:
        ```
        X-API-Key: sk_live_abc123xyz789def456ghi789jkl012mno345
        ```

  schemas:
    # ============ REQUEST SCHEMAS ============
    CreateJobRequest:
      type: object
      required:
        - leads
        - field
      properties:
        leads:
          type: array
          minItems: 1
          maxItems: 100
          description: Array de leads a enriquecer (máximo 100)
          items:
            $ref: '#/components/schemas/LeadInput'
        field:
          type: string
          enum: [email, phone]
          description: |
            Campo a buscar. Solo un campo por request:
            - `email`: Busca email profesional
            - `phone`: Busca teléfono profesional
        webhook_url:
          type: string
          format: uri
          maxLength: 500
          description: |
            URL donde enviar notificación al completar el job.
            Si no se envía, usa el webhook por defecto configurado.
            Si no hay ninguno, debes usar polling.
          example: "https://tu-servidor.com/webhooks/masleads"
        webhook_secret:
          type: string
          minLength: 16
          maxLength: 100
          description: |
            Secreto para firmar el webhook con HMAC-SHA256.
            Si no se envía, el webhook no incluirá firma.
          example: "mi_secreto_super_seguro_123"

    LeadInput:
      type: object
      required:
        - linkedin_url
      properties:
        linkedin_url:
          type: string
          format: uri
          description: |
            URL del perfil de LinkedIn. **Campo obligatorio.**
            Se normaliza automáticamente al formato canónico.
          example: "https://linkedin.com/in/johndoe"
        external_id:
          type: string
          maxLength: 100
          description: |
            Tu ID interno para este lead. Se devuelve en los resultados
            para facilitar el mapeo con tu sistema.
          example: "lead-123"
        name:
          type: string
          description: Nombre del lead. Mejora la búsqueda de email.
          example: "John"
        last_name:
          type: string
          description: Apellido del lead. Mejora la búsqueda de email.
          example: "Doe"
        company:
          type: string
          description: Nombre de la empresa del lead.
          example: "Acme Corp"
        domain:
          type: string
          description: |
            Dominio de la empresa. **Altamente recomendado** -
            mejora significativamente la tasa de éxito para emails corporativos.
          example: "acme.com"

    # ============ RESPONSE SCHEMAS ============
    JobCreated:
      type: object
      properties:
        job_id:
          type: string
          description: ID único del job
          example: "job_e3351fb3e67a431da96d17dec9"
        status:
          type: string
          enum: [pending]
          example: "pending"
        field:
          type: string
          enum: [email, phone]
          example: "email"
        total_leads:
          type: integer
          description: Número de leads en el job
          example: 2
        credits_charged:
          type: integer
          description: Créditos cobrados (se reembolsarán los no encontrados)
          example: 2
        created_at:
          type: string
          format: date-time
          example: "2026-01-09T10:13:57.356979"
        webhook_url:
          type: string
          format: uri
          nullable: true
          description: URL del webhook configurado para este job
          example: "https://yourapp.com/webhooks/masleads"
        webhook_secret_set:
          type: boolean
          description: Indica si se ha configurado un webhook_secret para firmar las notificaciones
          example: false

    JobStatus:
      type: object
      properties:
        job_id:
          type: string
          example: "job_01HQ3K5P7RJXYZ123ABC456"
        status:
          type: string
          enum: [pending, processing, completed, failed]
          description: |
            Estado del job:
            - `pending`: En cola, esperando procesamiento
            - `processing`: Procesando leads
            - `completed`: Completado exitosamente
            - `failed`: Falló (ver error_code)
        field:
          type: string
          enum: [email, phone]
        total_leads:
          type: integer
          description: Total de leads en el job
        leads_processed:
          type: integer
          description: Leads procesados hasta el momento
        leads_found:
          type: integer
          description: Leads con dato encontrado
        leads_not_found:
          type: integer
          description: Leads sin dato (créditos reembolsados)
        leads_error:
          type: integer
          description: Leads con error de procesamiento (créditos reembolsados)
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        completed_at:
          type: string
          format: date-time
          nullable: true
          description: Fecha de completado (null si no ha terminado)

    JobListResponse:
      type: object
      properties:
        jobs:
          type: array
          items:
            $ref: '#/components/schemas/JobStatus'
        pagination:
          $ref: '#/components/schemas/Pagination'

    JobResults:
      type: object
      properties:
        job_id:
          type: string
        status:
          type: string
          enum: [completed]
        field:
          type: string
          enum: [email, phone]
        total_leads:
          type: integer
        results:
          type: object
          properties:
            found:
              type: integer
              description: Leads con dato encontrado
            not_found:
              type: integer
              description: Leads sin dato
            error:
              type: integer
              description: Leads con error
        credits:
          type: object
          properties:
            charged:
              type: integer
              description: Créditos cobrados inicialmente
            refunded:
              type: integer
              description: Créditos reembolsados (not_found + error)
            net:
              type: integer
              description: Créditos netos consumidos
        leads:
          type: array
          items:
            $ref: '#/components/schemas/LeadResult'
        pagination:
          $ref: '#/components/schemas/Pagination'
        created_at:
          type: string
          format: date-time
        completed_at:
          type: string
          format: date-time

    LeadResult:
      type: object
      properties:
        external_id:
          type: string
          nullable: true
          description: Tu ID interno (si lo enviaste)
        linkedin_url:
          type: string
          description: URL normalizada del perfil
        status:
          type: string
          enum: [found, not_found, error]
          description: |
            Estado del lead:
            - `found`: Dato encontrado
            - `not_found`: No se encontró (crédito reembolsado)
            - `error`: Error de procesamiento (crédito reembolsado)
        result:
          type: string
          nullable: true
          description: |
            El dato encontrado (email o teléfono).
            `null` si status es `not_found` o `error`.
          example: "john.doe@acme.com"
        error_code:
          type: string
          nullable: true
          description: Código de error si status es `error`
          example: "PROFILE_NOT_ACCESSIBLE"
        refunded:
          type: boolean
          description: Si el crédito fue reembolsado
        request_uuid:
          type: string
          format: uuid
          description: UUID de la solicitud interna (para soporte)

    UsageResponse:
      type: object
      properties:
        credits_available:
          type: integer
          description: Créditos disponibles actualmente
          example: 10086
        credits_limit_monthly:
          type: integer
          description: Límite mensual de créditos según tu plan
          example: 1000
        credits_used_this_month:
          type: integer
          description: Créditos usados este mes
          example: 0
        period_start:
          type: string
          format: date-time
          description: Inicio del período de facturación actual
          example: "2026-01-01T00:00:00"
        period_end:
          type: string
          format: date-time
          description: Fin del período de facturación actual
          example: "2026-02-01T10:10:41.946954"
        plan:
          type: string
          description: Tu plan actual
          example: "standard"

    # ============ API KEY SCHEMAS ============
    ApiKeyCreate:
      type: object
      required:
        - name
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 50
          pattern: '^[a-zA-Z0-9_-]+$'
          description: |
            Nombre descriptivo para la API Key.
            Solo caracteres alfanuméricos, guiones y guiones bajos.
          example: "production-backend"

    ApiKeyCreated:
      type: object
      description: |
        Respuesta al crear una API Key. **Importante**: El campo `api_key`
        solo se muestra una vez. Guárdalo de forma segura.
      properties:
        key_id:
          type: string
          description: ID único de la key (para referencia y revocación)
          example: "key_abc123xyz789"
        name:
          type: string
          description: Nombre descriptivo de la key
          example: "production-backend"
        api_key:
          type: string
          description: |
            **La API Key completa. Solo se muestra una vez.**
            Guárdala de forma segura inmediatamente.
          example: "sk_live_abc123xyz789def456ghi789jkl012mno345"
        created_at:
          type: string
          format: date-time
          example: "2026-01-09T10:30:00"
        last_used_at:
          type: string
          format: date-time
          nullable: true
          description: Fecha del último uso (null si nunca usada)
          example: null

    ApiKey:
      type: object
      description: Información de una API Key (sin el valor completo)
      properties:
        key_id:
          type: string
          description: ID único de la key
          example: "key_abc123xyz789"
        name:
          type: string
          description: Nombre descriptivo
          example: "production-backend"
        key_preview:
          type: string
          description: Últimos 4 caracteres de la key (para identificación)
          example: "...o345"
        status:
          type: string
          enum: [active, revoked]
          description: Estado de la key
          example: "active"
        created_at:
          type: string
          format: date-time
          example: "2026-01-09T10:30:00"
        last_used_at:
          type: string
          format: date-time
          nullable: true
          description: Fecha del último uso
          example: "2026-01-09T14:22:15"
        revoked_at:
          type: string
          format: date-time
          nullable: true
          description: Fecha de revocación (null si activa)
          example: null

    ApiKeyList:
      type: object
      properties:
        api_keys:
          type: array
          items:
            $ref: '#/components/schemas/ApiKey'
        total:
          type: integer
          description: Total de API Keys
          example: 3
        max_allowed:
          type: integer
          description: Máximo de keys permitidas
          example: 10

    Pagination:
      type: object
      properties:
        page:
          type: integer
          example: 1
        per_page:
          type: integer
          example: 50
        total_leads:
          type: integer
          description: Total de leads en el job
          example: 1
        total_pages:
          type: integer
          example: 1

    # ============ WEBHOOK SCHEMAS ============
    WebhookConfig:
      type: object
      properties:
        webhook_url:
          type: string
          format: uri
          nullable: true
          description: URL del webhook por defecto
          example: "https://tu-servidor.com/webhooks/masleads"
        webhook_secret:
          type: string
          nullable: true
          description: |
            Secreto para firmar webhooks con HMAC-SHA256.
            Solo se muestra si está configurado (como `****` por seguridad).
          example: "****"
        is_configured:
          type: boolean
          description: Indica si hay un webhook configurado
          example: true
        last_triggered_at:
          type: string
          format: date-time
          nullable: true
          description: Última vez que se envió un webhook
          example: "2026-01-09T14:30:00"
        last_status_code:
          type: integer
          nullable: true
          description: Código HTTP de la última respuesta
          example: 200
        created_at:
          type: string
          format: date-time
          description: Fecha de configuración
          example: "2026-01-05T10:00:00"
        updated_at:
          type: string
          format: date-time
          description: Última actualización
          example: "2026-01-09T12:00:00"

    WebhookCreate:
      type: object
      required:
        - webhook_url
      properties:
        webhook_url:
          type: string
          format: uri
          maxLength: 500
          description: URL donde enviar las notificaciones
          example: "https://tu-servidor.com/webhooks/masleads"
        webhook_secret:
          type: string
          minLength: 16
          maxLength: 100
          description: |
            Secreto para firmar el payload con HMAC-SHA256.
            Mínimo 16 caracteres. Recomendado: 32+ caracteres aleatorios.
          example: "mi_secreto_webhook_muy_seguro_123456"

    WebhookUpdate:
      type: object
      properties:
        webhook_url:
          type: string
          format: uri
          maxLength: 500
          description: Nueva URL del webhook
          example: "https://nuevo-servidor.com/webhooks/masleads"
        webhook_secret:
          type: string
          minLength: 16
          maxLength: 100
          description: Nuevo secreto para firmar webhooks
          example: "nuevo_secreto_muy_seguro_456789"

    WebhookTestResult:
      type: object
      properties:
        success:
          type: boolean
          description: Si el webhook respondió correctamente
          example: true
        status_code:
          type: integer
          description: Código HTTP de respuesta
          example: 200
        response_time_ms:
          type: integer
          description: Tiempo de respuesta en milisegundos
          example: 245
        message:
          type: string
          description: Descripción del resultado
          example: "Webhook test successful"
        tested_at:
          type: string
          format: date-time
          example: "2026-01-09T15:00:00"

    # ============ ERROR SCHEMAS ============
    Error:
      type: object
      required:
        - error
        - message
      properties:
        error:
          type: string
          description: Código de error
        message:
          type: string
          description: Descripción del error
        details:
          type: object
          description: Detalles adicionales del error

    ValidationError:
      allOf:
        - $ref: '#/components/schemas/Error'
        - type: object
          properties:
            details:
              type: array
              items:
                type: object
                properties:
                  field:
                    type: string
                  message:
                    type: string

  responses:
    Unauthorized:
      description: |
        API Key inválida, revocada o expirada.

        Códigos de error:
        - `unauthorized`: API Key no enviada
        - `invalid_api_key`: API Key inválida
        - `api_key_revoked`: API Key revocada
        - `api_key_expired`: API Key expirada
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            missing:
              value:
                error: unauthorized
                message: "API Key required in X-API-Key header"
            invalid:
              value:
                error: invalid_api_key
                message: "The API key provided is invalid."
            revoked:
              value:
                error: api_key_revoked
                message: "This API key has been revoked."

    PaymentRequired:
      description: Créditos insuficientes
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: insufficient_credits
            message: "Not enough credits. Available: 50, Required: 500"

    BadRequest:
      description: Error de validación
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ValidationError'
          examples:
            validation:
              value:
                error: validation_error
                message: "Validation failed"
                details:
                  - field: "leads[0].linkedin_url"
                    message: "Required field"
            max_leads:
              value:
                error: max_leads_exceeded
                message: "Maximum 100 leads per request"
            duplicate:
              value:
                error: duplicate_linkedin_url
                message: "Duplicate linkedin_url found in request"
                details:
                  - linkedin_url: "https://www.linkedin.com/in/johndoe/"
                    positions: [2, 5]

    RateLimited:
      description: |
        Límite de rate excedido o máximo de jobs concurrentes.

        Espera el tiempo indicado en `Retry-After` antes de reintentar.
      headers:
        Retry-After:
          schema:
            type: integer
          description: Segundos a esperar antes de reintentar
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            rate_limit:
              value:
                error: rate_limit_exceeded
                message: "Rate limit exceeded. Maximum 300 requests per minute."
            concurrent_jobs:
              value:
                error: max_concurrent_jobs_exceeded
                message: "Maximum 10 concurrent jobs allowed. Wait for existing jobs to complete."

    NotFound:
      description: Job no encontrado
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: job_not_found
            message: "Job not found"

paths:
  /api-keys:
    get:
      tags:
        - API Keys
      summary: Listar API Keys
      description: |
        Lista todas las API Keys de tu empresa.

        Por seguridad, solo se muestran los últimos 4 caracteres de cada key (`key_preview`).
        Para ver la key completa, debes crearla nuevamente.
      operationId: listApiKeys
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum: [active, revoked, all]
            default: active
          description: Filtrar por estado de la key
      responses:
        '200':
          description: Lista de API Keys
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyList'
              example:
                api_keys:
                  - key_id: "key_abc123xyz789"
                    name: "production-backend"
                    key_preview: "...o345"
                    status: "active"
                    created_at: "2026-01-09T10:30:00"
                    last_used_at: "2026-01-09T14:22:15"
                    revoked_at: null
                  - key_id: "key_def456uvw123"
                    name: "staging-server"
                    key_preview: "...x789"
                    status: "active"
                    created_at: "2026-01-05T08:15:00"
                    last_used_at: null
                    revoked_at: null
                total: 2
                max_allowed: 10
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'

    post:
      tags:
        - API Keys
      summary: Crear nueva API Key
      description: |
        Crea una nueva API Key para tu empresa.

        ## Importante

        - La API Key completa (`api_key`) **solo se muestra una vez** en la respuesta.
        - Guárdala de forma segura inmediatamente.
        - Si la pierdes, deberás crear una nueva key.
        - Máximo 10 keys activas por empresa.
      operationId: createApiKey
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ApiKeyCreate'
            example:
              name: "production-backend"
      responses:
        '201':
          description: API Key creada exitosamente
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyCreated'
              example:
                key_id: "key_abc123xyz789"
                name: "production-backend"
                api_key: "sk_live_abc123xyz789def456ghi789jkl012mno345"
                created_at: "2026-01-09T10:30:00"
                last_used_at: null
        '400':
          description: Error de validación
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                invalid_name:
                  value:
                    error: "validation_error"
                    message: "Invalid key name. Only alphanumeric, hyphens and underscores allowed."
                max_keys:
                  value:
                    error: "max_api_keys_exceeded"
                    message: "Maximum 10 API keys allowed per company. Revoke unused keys first."
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'

  /api-keys/{key_id}:
    get:
      tags:
        - API Keys
      summary: Obtener detalles de API Key
      description: |
        Obtiene los detalles de una API Key específica.

        Por seguridad, solo se muestra el preview de la key (últimos 4 caracteres).
      operationId: getApiKey
      parameters:
        - name: key_id
          in: path
          required: true
          schema:
            type: string
          description: ID de la API Key
          example: "key_abc123xyz789"
      responses:
        '200':
          description: Detalles de la API Key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKey'
              example:
                key_id: "key_abc123xyz789"
                name: "production-backend"
                key_preview: "...o345"
                status: "active"
                created_at: "2026-01-09T10:30:00"
                last_used_at: "2026-01-09T14:22:15"
                revoked_at: null
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: API Key no encontrada
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: "api_key_not_found"
                message: "API key not found"
        '429':
          $ref: '#/components/responses/RateLimited'

    delete:
      tags:
        - API Keys
      summary: Revocar API Key
      description: |
        Revoca una API Key. **Esta acción es irreversible.**

        Una vez revocada, la key deja de funcionar **inmediatamente**.
        Todas las solicitudes con esa key recibirán error `401 Unauthorized`.

        ## Recomendación

        Antes de revocar, asegúrate de que ningún sistema esté usando esta key,
        o que ya hayas actualizado a una nueva key.
      operationId: revokeApiKey
      parameters:
        - name: key_id
          in: path
          required: true
          schema:
            type: string
          description: ID de la API Key a revocar
          example: "key_abc123xyz789"
      responses:
        '200':
          description: API Key revocada exitosamente
          content:
            application/json:
              schema:
                type: object
                properties:
                  key_id:
                    type: string
                    example: "key_abc123xyz789"
                  status:
                    type: string
                    example: "revoked"
                  revoked_at:
                    type: string
                    format: date-time
                    example: "2026-01-09T15:00:00"
                  message:
                    type: string
                    example: "API key revoked successfully. It will no longer work for authentication."
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: API Key no encontrada
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: "api_key_not_found"
                message: "API key not found"
        '409':
          description: API Key ya revocada
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: "api_key_already_revoked"
                message: "This API key has already been revoked"
        '429':
          $ref: '#/components/responses/RateLimited'

  /enrich/leads:
    post:
      tags:
        - Enrichment
      summary: Crear job de enriquecimiento
      description: |
        Crea un nuevo job para enriquecer hasta 100 leads seleccionando el campo que quieres enriquecer (`email` o `phone`).

        ## Funcionamiento

        1. Los créditos se cobran inmediatamente al crear el job
        2. El job se procesa de forma asíncrona
        3. Los leads no encontrados reciben reembolso automático
        4. Recibirás notificación via webhook (si configurado) o puedes usar polling



        ## Webhook vs Polling

        - **Webhook** (recomendado): Configura `webhook_url` para recibir notificación automática
        - **Polling**: Consulta `GET /jobs/{job_id}` cada 30 segundos hasta que `status` sea `completed`
      operationId: createEnrichmentJob
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateJobRequest'
            examples:
              email_simple:
                summary: Enriquecer emails (básico)
                value:
                  field: "email"
                  leads:
                    - linkedin_url: "https://www.linkedin.com/in/johndue/"
                      external_id: "Lead-001"
                    - linkedin_url: "https://www.linkedin.com/in/foobar/"
                      external_id: "Lead-002"
              email_with_context:
                summary: Enriquecer emails (con contexto)
                value:
                  field: "email"
                  leads:
                    - linkedin_url: "https://www.linkedin.com/in/johndue/"
                      external_id: "Lead-001"
                      name: "John"
                      last_name: "Doe"
                      company: "Acme Corp"
                      domain: "acme.com"
              with_webhook:
                summary: Con webhook personalizado
                value:
                  field: "email"
                  leads:
                    - linkedin_url: "https://www.linkedin.com/in/johndue/"
                      external_id: "Lead-001"
                    - linkedin_url: "https://www.linkedin.com/in/foobar/"
                      external_id: "Lead-002"
                  webhook_url: "https://yourapp.com/webhooks/masleads"
      responses:
        '202':
          description: Job creado exitosamente
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobCreated'
              example:
                job_id: "job_e3351fb3e67a431da96d17dec9"
                status: "pending"
                field: "email"
                total_leads: 2
                credits_charged: 2
                created_at: "2026-01-09T10:13:57.356979"
                webhook_url: "https://yourapp.com/webhooks/masleads"
                webhook_secret_set: false
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '429':
          $ref: '#/components/responses/RateLimited'

  /jobs:
    get:
      tags:
        - Jobs
      summary: Listar jobs
      description: |
        Lista todos tus jobs con paginación y filtros opcionales.

        Útil para:
        - Ver historial de jobs
        - Monitorear jobs en progreso
        - Filtrar por estado o tipo
      operationId: listJobs
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            default: 1
            minimum: 1
            maximum: 10000
          description: Número de página
        - name: per_page
          in: query
          schema:
            type: integer
            default: 20
            minimum: 1
            maximum: 100
          description: Resultados por página
        - name: status
          in: query
          schema:
            type: string
            enum: [pending, processing, completed, failed]
          description: Filtrar por estado
        - name: field
          in: query
          schema:
            type: string
            enum: [email, phone]
          description: Filtrar por tipo de campo
        - name: created_after
          in: query
          schema:
            type: string
            format: date-time
          description: Jobs creados después de esta fecha (ISO 8601)
          example: "2025-01-01T00:00:00Z"
        - name: created_before
          in: query
          schema:
            type: string
            format: date-time
          description: Jobs creados antes de esta fecha (ISO 8601)
          example: "2025-01-31T23:59:59Z"
      responses:
        '200':
          description: Lista de jobs
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'

  /jobs/{job_id}:
    get:
      tags:
        - Jobs
      summary: Consultar estado de job (Polling)
      description: |
        Obtiene el estado actual de un job.

        ## Uso para Polling

        1. Crea el job con `POST /enrich/leads`
        2. Guarda el `job_id` devuelto
        3. Consulta este endpoint cada **30 segundos mínimo**
        4. Cuando `status` sea `completed`, llama a `GET /jobs/{job_id}/results`

        ## Estados

        | Estado | Descripción |
        |--------|-------------|
        | `pending` | En cola, esperando procesamiento |
        | `processing` | Procesando leads activamente |
        | `completed` | Completado - puedes obtener resultados |
        | `failed` | Error - consulta `error_code` |
      operationId: getJobStatus
      parameters:
        - name: job_id
          in: path
          required: true
          schema:
            type: string
            pattern: '^job_[A-Z0-9]{26}$'
          description: ID del job
          example: "job_01HQ3K5P7RJXYZ123ABC456"
      responses:
        '200':
          description: Estado del job
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobStatus'
              examples:
                processing:
                  summary: Job en progreso
                  value:
                    job_id: "job_5b719b92e9984ef1ae55daca5e"
                    status: "processing"
                    field: "email"
                    total_leads: 2
                    leads_processed: 0
                    leads_found: 0
                    leads_not_found: 0
                    leads_error: 0
                    created_at: "2026-01-09T10:15:50"
                    updated_at: "2026-01-09T10:15:51"
                    completed_at: null
                completed:
                  summary: Job completado
                  value:
                    job_id: "job_5b719b92e9984ef1ae55daca5e"
                    status: "completed"
                    field: "email"
                    total_leads: 2
                    leads_processed: 2
                    leads_found: 1
                    leads_not_found: 0
                    leads_error: 1
                    created_at: "2026-01-09T10:15:50"
                    updated_at: "2026-01-09T10:16:30"
                    completed_at: "2026-01-09T10:16:30"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

  /jobs/{job_id}/results:
    get:
      tags:
        - Jobs
      summary: Obtener resultados de job
      description: |
        Obtiene los resultados detallados de un job completado.

        **Importante:** Solo funciona cuando `status` es `completed`.
        Para jobs en progreso, retorna error 400.

        ## Respuesta

        Los leads se devuelven en el mismo orden en que fueron enviados.
        Cada lead incluye:
        - `status`: `found`, `not_found`, o `error`
        - `result`: El email/teléfono encontrado (o null)
        - `refunded`: Si el crédito fue reembolsado
        - `external_id`: Tu ID interno (si lo enviaste)
      operationId: getJobResults
      parameters:
        - name: job_id
          in: path
          required: true
          schema:
            type: string
          example: "job_01HQ3K5P7RJXYZ123ABC456"
        - name: page
          in: query
          schema:
            type: integer
            default: 1
          description: Número de página
        - name: per_page
          in: query
          schema:
            type: integer
            default: 50
            maximum: 100
          description: Leads por página
      responses:
        '200':
          description: Resultados del job
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobResults'
              example:
                job_id: "job_4d92187e7b144fafa8e21ad9f4"
                status: "completed"
                field: "email"
                total_leads: 1
                results:
                  found: 0
                  not_found: 0
                  error: 1
                credits:
                  charged: 1
                  refunded: 1
                  net: 0
                leads:
                  - external_id: "CRM-001"
                    linkedin_url: "https://www.linkedin.com/in/jquinterosmv2/"
                    status: "error"
                    error_code: "timeout"
                    result: null
                    refunded: true
                    request_uuid: "41e6636a-1ca4-407a-aec2-b10612795d5b"
                pagination:
                  page: 1
                  per_page: 50
                  total_leads: 1
                  total_pages: 1
                created_at: "2026-01-08T14:39:34"
                completed_at: "2026-01-08T15:10:12"
        '400':
          description: Job no completado todavía
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: job_not_completed
                message: "Results are only available for completed jobs"
                job_id: "job_01HQ3K5P7RJXYZ123ABC456"
                current_status: "processing"
                leads_processed: 75
                total_leads: 100
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

  /usage:
    get:
      tags:
        - Usage
      summary: Consultar créditos disponibles
      description: |
        Obtiene el balance de créditos actual y uso del mes.

        Usa este endpoint para verificar créditos antes de crear jobs
        o para mostrar el balance en tu aplicación.
      operationId: getUsage
      responses:
        '200':
          description: Balance de créditos
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UsageResponse'
              example:
                credits_available: 10086
                credits_limit_monthly: 1000
                credits_used_this_month: 0
                period_start: "2026-01-01T00:00:00"
                period_end: "2026-02-01T10:10:41.946954"
                plan: "standard"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'

  /webhooks:
    get:
      tags:
        - Webhooks
      summary: Obtener configuración de webhook
      description: |
        Obtiene la configuración actual del webhook por defecto.

        El webhook por defecto se usa cuando creas jobs sin especificar
        `webhook_url` en el request.
      operationId: getWebhookConfig
      responses:
        '200':
          description: Configuración del webhook
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookConfig'
              examples:
                configured:
                  summary: Webhook configurado
                  value:
                    webhook_url: "https://tu-servidor.com/webhooks/masleads"
                    webhook_secret: "****"
                    is_configured: true
                    last_triggered_at: "2026-01-09T14:30:00"
                    last_status_code: 200
                    created_at: "2026-01-05T10:00:00"
                    updated_at: "2026-01-09T12:00:00"
                not_configured:
                  summary: Sin webhook configurado
                  value:
                    webhook_url: null
                    webhook_secret: null
                    is_configured: false
                    last_triggered_at: null
                    last_status_code: null
                    created_at: null
                    updated_at: null
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'

    post:
      tags:
        - Webhooks
      summary: Configurar webhook por defecto
      description: |
        Configura el webhook por defecto para recibir notificaciones
        cuando tus jobs se completen.

        ## Payload del Webhook

        Cuando un job complete, enviaremos un POST a tu URL con:

        ```json
        {
          "event": "job.completed",
          "job_id": "job_abc123...",
          "status": "completed",
          "total_leads": 50,
          "leads_found": 45,
          "leads_not_found": 3,
          "leads_error": 2,
          "timestamp": "2026-01-09T15:00:00Z"
        }
        ```

        ## Firma HMAC (si configuras webhook_secret)

        El header `X-MasLeads-Signature` contiene la firma HMAC-SHA256
        del payload. Verifica la firma para asegurar autenticidad.
      operationId: createWebhookConfig
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookCreate'
            example:
              webhook_url: "https://tu-servidor.com/webhooks/masleads"
              webhook_secret: "mi_secreto_webhook_muy_seguro_123456"
      responses:
        '201':
          description: Webhook configurado exitosamente
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookConfig'
              example:
                webhook_url: "https://tu-servidor.com/webhooks/masleads"
                webhook_secret: "****"
                is_configured: true
                last_triggered_at: null
                last_status_code: null
                created_at: "2026-01-09T15:00:00"
                updated_at: "2026-01-09T15:00:00"
        '400':
          description: Error de validación
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                invalid_url:
                  value:
                    error: "validation_error"
                    message: "Invalid webhook URL. Must be HTTPS."
                short_secret:
                  value:
                    error: "validation_error"
                    message: "webhook_secret must be at least 16 characters"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          description: Ya existe un webhook configurado
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: "webhook_already_configured"
                message: "A webhook is already configured. Use PUT to update or DELETE to remove."
        '429':
          $ref: '#/components/responses/RateLimited'

    put:
      tags:
        - Webhooks
      summary: Actualizar webhook
      description: |
        Actualiza la configuración del webhook existente.

        Puedes actualizar solo la URL, solo el secreto, o ambos.
      operationId: updateWebhookConfig
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookUpdate'
            examples:
              update_url:
                summary: Actualizar solo URL
                value:
                  webhook_url: "https://nuevo-servidor.com/webhooks/masleads"
              update_secret:
                summary: Actualizar solo secreto
                value:
                  webhook_secret: "nuevo_secreto_muy_seguro_456789"
              update_both:
                summary: Actualizar ambos
                value:
                  webhook_url: "https://nuevo-servidor.com/webhooks/masleads"
                  webhook_secret: "nuevo_secreto_muy_seguro_456789"
      responses:
        '200':
          description: Webhook actualizado
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookConfig'
        '400':
          description: Error de validación
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: No hay webhook configurado
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: "webhook_not_configured"
                message: "No webhook configured. Use POST to create one."
        '429':
          $ref: '#/components/responses/RateLimited'

    delete:
      tags:
        - Webhooks
      summary: Eliminar webhook
      description: |
        Elimina la configuración del webhook por defecto.

        Después de eliminar, los jobs no enviarán notificaciones
        a menos que especifiques `webhook_url` en cada request.
      operationId: deleteWebhookConfig
      responses:
        '200':
          description: Webhook eliminado
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: "Webhook configuration deleted successfully"
                  deleted_at:
                    type: string
                    format: date-time
                    example: "2026-01-09T15:30:00"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: No hay webhook configurado
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: "webhook_not_configured"
                message: "No webhook configured"
        '429':
          $ref: '#/components/responses/RateLimited'

  /webhooks/test:
    post:
      tags:
        - Webhooks
      summary: Probar webhook
      description: |
        Envía un evento de prueba al webhook configurado.

        Útil para verificar que tu servidor recibe y procesa
        correctamente las notificaciones.

        ## Payload de prueba

        ```json
        {
          "event": "webhook.test",
          "message": "This is a test webhook from MasLeads",
          "timestamp": "2026-01-09T15:00:00Z"
        }
        ```
      operationId: testWebhook
      responses:
        '200':
          description: Resultado del test
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookTestResult'
              examples:
                success:
                  summary: Test exitoso
                  value:
                    success: true
                    status_code: 200
                    response_time_ms: 245
                    message: "Webhook test successful"
                    tested_at: "2026-01-09T15:00:00"
                failure:
                  summary: Test fallido
                  value:
                    success: false
                    status_code: 500
                    response_time_ms: 1234
                    message: "Webhook returned error status code"
                    tested_at: "2026-01-09T15:00:00"
                timeout:
                  summary: Timeout
                  value:
                    success: false
                    status_code: null
                    response_time_ms: 30000
                    message: "Webhook request timed out after 30 seconds"
                    tested_at: "2026-01-09T15:00:00"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: No hay webhook configurado
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: "webhook_not_configured"
                message: "No webhook configured. Configure one first with POST /webhooks"
        '429':
          $ref: '#/components/responses/RateLimited'
