> ## Documentation Index
> Fetch the complete documentation index at: https://docs.inagent.inconcertcx.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Crear un lote

> Crea un lote desde la API o a partir de un CSV previamente subido.

La respuesta confirma la creación del lote con su `batchId`, `status: "created"` y `source`. Esta operación no devuelve `operationId` ni el header `Location`.

Usa `source: "api"` para crear un lote vacío, sin subir un CSV. Debes incluir `duplicateStrategy` para definir cómo tratar los contactos repetidos; no admite `null` ni puede omitirse. También puedes incluir `rules` para configurar las reglas de marcación desde el inicio.

<Info>
  Después de crear un lote con `source: "api"`, añade los contactos mediante `POST /batches/{batchId}/contacts`. Esa carga sí devuelve un `operationId` para consultar su progreso.
</Info>


## OpenAPI

````yaml api-reference/dialer/openapi.yaml POST /batches
openapi: 3.1.0
info:
  title: AutoContact API
  version: final
  summary: Gestión de lotes de marcación automática y carga de contactos.
  description: >
    API de AutoContact: gestión de lotes de marcación automática y de los
    contactos que los integran.


    Todos los recursos se publican bajo `/autocontact/api/v1/`.


    ## Autenticación


    Las peticiones se autentican con el header `apikey`. La misma credencial
    permite operar los lotes

    de la cuenta a los que tenga acceso. El servicio deriva la cuenta de esa
    credencial y verifica que

    el recurso solicitado le pertenezca.


    Un lote que no pertenece a la cuenta responde `404`, igual que un lote
    inexistente.


    ## Operaciones asíncronas


    Algunas operaciones responden `202` en lugar de `200`: la petición se acepta
    y el trabajo continúa

    en segundo plano. Es el caso de la carga de contactos, el arranque y la
    eliminación de un lote.


    Esas respuestas incluyen un `operationId` que se consulta en

    `GET /batches/operations/{operationId}` para conocer el resultado.


    `POST /batches` no genera una operación: devuelve directamente el `batchId`,
    el estado `created` y

    el origen del lote, sin `operationId`.


    Tiempo de retención de las operaciones:


    | Tipo de operación | Disponible durante |

    |---|---|

    | Carga de contactos | 6 horas |

    | Ciclo de vida del lote (arranque, archivado, eliminación) | 1 hora, y 10
    minutos desde que finaliza |


    ## Reintentos seguros con `Idempotency-Key`


    Las operaciones de escritura aceptan el header opcional `Idempotency-Key`.
    Resuelve el caso en que

    se produce un error de red después de que la petición llegó al servicio: sin
    este header no es

    posible determinar si el trabajo se procesó, y reintentar podría duplicar
    llamadas telefónicas.


    Con el header, un reintento que use la misma clave y el mismo cuerpo
    devuelve la respuesta original

    sin procesar nada de nuevo. Las claves se conservan 24 horas.


    Recomendación de uso: generar un identificador único (por ejemplo un UUID)
    antes del primer intento

    y reutilizarlo solo en los reintentos de esa misma operación.


    Requisitos de la clave: entre 1 y 255 caracteres, solo ASCII imprimible. Una
    clave que no cumpla

    estos requisitos se rechaza con `400 INVALID_IDEMPOTENCY_KEY` en lugar de
    ignorarse, para evitar

    procesar la petición sin la protección que el cliente espera tener.


    ### Headers de respuesta


    | Header | Cuándo aparece | Significado |

    |---|---|---|

    | `Idempotency-Replayed: true` | En un reintento | La respuesta corresponde
    a la petición original; no se procesó de nuevo |

    | `Retry-After` | Junto a `409 IDEMPOTENCY_KEY_IN_FLIGHT` | Segundos a
    esperar antes de reintentar |

    | `Idempotency-Status: bypassed` | Excepcionalmente | La petición se procesó
    **sin** protección de idempotencia |


    El header `Idempotency-Status: bypassed` indica que el servicio de
    idempotencia no estaba

    disponible y la petición se procesó igualmente, para no interrumpir la
    operación. La respuesta es

    válida, pero esa petición concreta no quedó protegida: si no se recibe
    respuesta, se recomienda

    verificar el estado antes de reintentar.


    Las respuestas con error `5xx` no se almacenan, de modo que un reintento con
    la misma clave vuelve a

    procesar la petición.
servers:
  - url: https://api.backend.inconcertcc.com/autocontact/api/v1
    description: API de Inagent
security:
  - ApiKeyAuth: []
tags:
  - name: Lotes
    description: |
      Creación, consulta y control de lotes de marcación.
  - name: Contactos
    description: |
      Carga de contactos en un lote, reagendamiento y anulación.
  - name: Direcciones bloqueadas
    description: |
      Direcciones excluidas de la marcación en toda la cuenta.
  - name: Operaciones
    description: |
      Seguimiento de las operaciones asíncronas que devuelven `202`.
paths:
  /batches:
    post:
      tags:
        - Lotes
      summary: Crea un lote desde la API o a partir de un CSV
      description: >
        Crea un lote vacío para cargar contactos mediante la API, o crea el lote
        e inicia la importación

        de los contactos de un CSV. La respuesta identifica el lote creado; no
        incluye `operationId`.


        ## Crear un lote sin CSV


        Indique `source: api` y omita `tempCsvKey` y `mapping`. Incluya
        `duplicateStrategy`, que es

        obligatorio, y defina `rules` en la misma petición si necesita reglas
        propias. Después, cargue los contactos con

        `POST /batches/{batchId}/contacts`.


        ## Crear un lote desde un CSV


        1. `POST /batches/upload-url` para obtener la URL de subida y la clave
        del archivo.

        2. `PUT` del CSV a esa URL, con `Content-Type: text/csv`.

        3. Esta operación, indicando el `tempCsvKey` obtenido y el `mapping` de
        columnas.

        4. Seguimiento de la importación mediante `GET /batches/{batchId}`,
        usando el `batchId` devuelto.


        ## Configuración del mapeo


        El campo `mapping` determina qué columna del CSV corresponde a cada
        dato. Se recomienda revisarlo

        con atención: un mapeo incorrecto no produce un error, sino un lote que
        marca los números de una

        columna equivocada.


        Cada entrada de `mapping.addresses` indica una columna y su prioridad de
        marcación, donde `1` es

        el primer intento. Las prioridades deben ser distintas entre sí.


        La existencia de las columnas en el archivo no se comprueba en esta
        operación, sino durante la

        importación. Un mapeo que apunte a una columna inexistente devuelve
        `202`; revise el resultado

        de la importación en el propio lote.


        ## Validación


        La respuesta `400` incluye todos los errores detectados, cada uno con su
        campo y un código

        estable.


        ## Seguimiento de la importación


        La respuesta no incluye `operationId` ni el header `Location`. Consulte

        `GET /batches/{batchId}` con el identificador devuelto: `uploadStatus`
        indica si la importación

        finalizó, `uploadProgress` muestra su avance y `totalContacts`, cuántos
        contactos se cargaron.
      operationId: createBatch
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateBatchInput'
            examples:
              desdeApi:
                summary: Lote sin CSV y con reglas personalizadas
                value:
                  name: Lote lunes 09-21
                  channelId: 1c8ade96-4ce0-40fa-90b0-30916df5e673
                  source: api
                  duplicateStrategy:
                    enabled: true
                    detectionCriteria: identifier
                    action: update
                  rules:
                    strategy:
                      maxAttemptsPerContact: 1
                      maxAttemptsPerAddress: 1
                      timeBetweenAttempts: 1
                      scheduleStart: '09:00'
                      scheduleEnd: '23:50'
                      timezone: America/Bogota
                      daysOfWeek:
                        - 1
                      processAllContactsFirst: true
                    addressPriority:
                      tryAllBeforeRetry: true
                    resultRules:
                      noAnswer:
                        retryDelay: 1
                        maxRetries: 1
                      busy:
                        retryDelay: 1
                        maxRetries: 1
                      voicemail:
                        retryDelay: 1
                        maxRetries: 1
                    amd:
                      amdEnabled: true
                      amdAction: HANGUP
              minimo:
                summary: Lote desde un CSV
                value:
                  name: Cobranzas agosto
                  channelId: 8c1e9f30-2a4b-4d6e-9f10-3b7c5d2e8a41
                  duplicateStrategy:
                    enabled: true
                    detectionCriteria: identifier
                    action: reject
                  tempCsvKey: >-
                    csv-uploads/acme/3f8a1b2c-5d6e-4f70-8a9b-1c2d3e4f5a6b-contactos-agosto.csv
                  mapping:
                    identifier: id_cliente
                    name: nombre
                    addresses:
                      - column: telefono
                        priority: 1
              completo:
                summary: Con filtros y deduplicación
                value:
                  name: Cobranzas agosto
                  channelId: 8c1e9f30-2a4b-4d6e-9f10-3b7c5d2e8a41
                  channelPriority: 5
                  tempCsvKey: >-
                    csv-uploads/acme/3f8a1b2c-5d6e-4f70-8a9b-1c2d3e4f5a6b-contactos-agosto.csv
                  mapping:
                    identifier: id_cliente
                    name: nombre
                    addresses:
                      - column: telefono
                        priority: 1
                      - column: celular
                        priority: 2
                    customFields:
                      - saldo
                      - sucursal
                  filters:
                    enabled: true
                    conditions:
                      - column: saldo
                        operator: greater_than
                        value: '1000'
                  duplicateStrategy:
                    enabled: true
                    detectionCriteria: phone
                    action: reject
      responses:
        '202':
          description: >
            Lote creado. Si se indicó un CSV, su importación ocurre en segundo
            plano y puede seguirse

            consultando el lote mediante su `batchId`.
          content:
            application/json:
              schema:
                type: object
                required:
                  - batchId
                  - status
                  - source
                properties:
                  batchId:
                    type: string
                    format: uuid
                  status:
                    type: string
                    enum:
                      - created
                  source:
                    type: string
                    description: Origen del lote.
                    example: api
              example:
                batchId: 3f8a1b2c-5d6e-4f70-8a9b-1c2d3e4f5a6b
                status: created
                source: api
        '400':
          description: >
            El cuerpo no pasó la validación, el canal está deshabilitado para la
            cuenta, o se supera el

            límite de contactos.


            Cuando la petición usa un CSV, solo `VALIDATION_FAILED` permite
            reintentar con la misma

            clave de archivo. En los otros dos casos el CSV se descarta y debe
            subirse de nuevo.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/ValidationError'
                  - $ref: '#/components/schemas/ContactLimitError'
                  - $ref: '#/components/schemas/SimpleError'
              examples:
                validacion:
                  summary: Errores de validación
                  value:
                    code: VALIDATION_FAILED
                    message: Validación fallida en el cuerpo del request
                    details:
                      - field: mapping.addresses[1].priority
                        code: DUPLICATED_IN_REQUEST
                        reason: >-
                          duplicated priority: each address column needs a
                          distinct dialing order
                        meta:
                          priority: 1
                      - field: filters.conditions[0].operator
                        code: INVALID_TYPE
                        reason: >-
                          must be one of: equals, not_equals, contains,
                          not_contains, starts_with, ends_with, is_empty,
                          not_empty, greater_than, less_than, greater_or_equal,
                          less_or_equal
                        meta:
                          received: mayor_que
                estrategiaDuplicadosObligatoria:
                  summary: Falta la estrategia de duplicados
                  value:
                    code: VALIDATION_FAILED
                    message: Validación fallida en el cuerpo del request
                    details:
                      - field: duplicateStrategy
                        code: REQUIRED
                        reason: >-
                          required: it decides what happens when a contact
                          already in the batch is sent again; duplicate
                          detection cannot be turned off
                canalDeshabilitado:
                  summary: Canal deshabilitado para la cuenta
                  value:
                    code: CHANNEL_DISABLED
                    message: >-
                      The channel type "voice" is disabled for this tenant
                      (max_channels = 0).
                limite:
                  summary: Límite de contactos excedido
                  value:
                    code: CONTACT_LIMIT_EXCEEDED
                    message: >-
                      Contact limit exceeded. You are trying to add 5000
                      contacts, but you can only add 120 more.
                    maxContacts: 10000
                    currentActive: 9880
                    attempting: 5000
                    exceededBy: 4880
        '500':
          $ref: '#/components/responses/InternalError'
components:
  schemas:
    CreateBatchInput:
      type: object
      required:
        - name
        - channelId
        - duplicateStrategy
      oneOf:
        - title: Lote desde un CSV
          required:
            - tempCsvKey
            - mapping
          not:
            required:
              - source
        - title: Lote desde la API
          required:
            - source
          properties:
            source:
              const: api
      properties:
        name:
          type: string
          maxLength: 100
          description: >
            Nombre del lote. Tiene que ser único en la cuenta: repetirlo
            responde `400`.
          example: Cobranzas agosto
        channelId:
          type: string
          format: uuid
          description: Canal a través del cual se realizarán las llamadas.
        channelPriority:
          type: integer
          minimum: 1
          maximum: 10
          default: 1
          description: >
            Peso del lote al repartir la capacidad del canal entre lotes que
            corren a la vez. Más

            alto recibe más capacidad.
        source:
          type: string
          enum:
            - api
          description: >
            Indica que el lote se crea vacío y que sus contactos se cargarán
            mediante la API. Omítalo

            cuando el lote se cree desde un CSV.
        tempCsvKey:
          type: string
          description: >
            Clave del CSV ya subido, tal como la devuelve `POST
            /batches/upload-url` en su

            campo `tempCsvKey`.


            Se consume al crear el lote: no sirve para un segundo alta.
          example: >-
            csv-uploads/acme/3f8a1b2c-5d6e-4f70-8a9b-1c2d3e4f5a6b-contactos-agosto.csv
        expectedRowCount:
          type: integer
          description: >
            Número de filas previsto en el archivo. Se utiliza únicamente para
            estimar el progreso de la

            importación antes de terminar de contarlo; no valida nada.
        mapping:
          $ref: '#/components/schemas/BatchMapping'
        filters:
          $ref: '#/components/schemas/BatchFilters'
        duplicateStrategy:
          $ref: '#/components/schemas/DuplicateStrategy'
          description: >
            Política ante contactos repetidos. Es obligatoria y no admite
            `null`: la detección de

            duplicados no puede desactivarse omitiendo este campo.
        rules:
          $ref: '#/components/schemas/BatchRules'
          description: >
            Reglas de marcación propias del lote (intentos, esperas, ventana
            horaria). Omitido, el

            lote hereda las reglas de la cuenta para su tipo de canal.
    ValidationError:
      type: object
      required:
        - message
      properties:
        code:
          type: string
          enum:
            - VALIDATION_FAILED
          description: >
            Presente cuando el rechazo corresponde a la validación de contactos.
            No aparece en los

            rechazos por límites de la petición, como un cuerpo vacío o más de
            1000 contactos.
        message:
          type: string
        details:
          description: >
            Detalle del rechazo. La estructura varía según el caso: una lista de
            textos para los límites

            de la petición, o una lista de `FieldRejection` cuando el rechazo es
            de validación de

            contactos.
          oneOf:
            - type: array
              items:
                type: string
            - type: array
              items:
                $ref: '#/components/schemas/FieldRejection'
    ContactLimitError:
      type: object
      required:
        - code
        - message
      description: >
        Límite de contactos activos de la cuenta excedido. Es el único error de
        estos endpoints que

        trae un `code` estable.
      properties:
        code:
          type: string
          enum:
            - CONTACT_LIMIT_EXCEEDED
        message:
          type: string
        maxContacts:
          type: integer
          description: Tope de contactos activos de la cuenta.
        currentActive:
          type: integer
          description: Cuántos hay activos ahora.
        attempting:
          type: integer
          description: Cuántos intentabas agregar.
        exceededBy:
          type: integer
          description: Por cuántos se excede el tope.
    SimpleError:
      type: object
      required:
        - message
      description: >
        Error con mensaje descriptivo.


        Algunos traen `code` estable (los más nuevos); otros solo `message`.


        Los mensajes pueden aparecer en español o en inglés según el caso, y
        algunos errores todavía no

        incluyen `code`. Cuando esté presente, utilice `code` para tratar el
        error de forma programática.
      properties:
        code:
          type: string
          description: >
            Presente en los errores ya normalizados (`MISSING_PARAMETER`,
            `OPERATION_NOT_FOUND`,

            `VALIDATION_FAILED`, `INVALID_IDEMPOTENCY_KEY`, y los de
            idempotencia).
        message:
          type: string
    BatchMapping:
      type: object
      required:
        - identifier
        - name
        - addresses
      description: |
        Qué columna del CSV es cada cosa.

        Los nombres son los del **encabezado del archivo**, tal como figuran.
      properties:
        identifier:
          type: string
          description: >
            Columna que contiene el identificador del contacto en el sistema de
            origen. Permite

            relacionar los resultados con los registros propios.
          example: id_cliente
        name:
          type: string
          description: Columna con el nombre del contacto.
          example: nombre
        addresses:
          type: array
          minItems: 1
          maxItems: 10
          description: >
            Columnas con los teléfonos o emails, cada una con su orden de
            marcación.
          items:
            type: object
            required:
              - column
              - priority
            properties:
              column:
                type: string
                description: >
                  Columna con la dirección. No se puede repetir entre entradas:
                  marcaría el mismo

                  número dos veces, consumiendo intentos del límite.
                example: telefono
              priority:
                type: integer
                minimum: 1
                description: >
                  Orden de marcación: **1 se intenta primero**. Tiene que ser
                  distinta en cada

                  entrada — dos iguales dejarían el orden indefinido.
                example: 1
        priority:
          type: string
          description: >
            Columna opcional con la prioridad del contacto, para que unos se
            marquen antes que otros.
        customFields:
          type: array
          items:
            type: string
          description: >
            Columnas adicionales que se almacenan con cada contacto. No
            intervienen en la marcación y

            quedan disponibles para el asistente.
          example:
            - saldo
            - sucursal
    BatchFilters:
      type: object
      required:
        - enabled
      description: >
        Filtra qué filas del CSV se importan. Sin filtros se importa el archivo
        completo.
      properties:
        enabled:
          type: boolean
        conditions:
          type: array
          items:
            type: object
            required:
              - column
              - operator
            properties:
              column:
                type: string
                description: Columna del CSV sobre la que se filtra.
              operator:
                type: string
                enum:
                  - equals
                  - not_equals
                  - contains
                  - not_contains
                  - starts_with
                  - ends_with
                  - is_empty
                  - not_empty
                  - greater_than
                  - less_than
                  - greater_or_equal
                  - less_or_equal
              value:
                type: string
                description: >
                  Valor contra el que se compara. Obligatorio salvo con
                  `is_empty` y `not_empty`, que

                  no comparan contra nada.
    DuplicateStrategy:
      type: object
      required:
        - enabled
      description: >
        Qué hacer cuando un contacto que se incorpora al lote ya existe en la
        cuenta.


        `detectionCriteria` y `action` solo se exigen con `enabled: true`.
      properties:
        enabled:
          type: boolean
        detectionCriteria:
          type: string
          enum:
            - identifier
            - phone
            - email
          description: Por qué campo se considera que dos contactos son el mismo.
        action:
          type: string
          enum:
            - reject
            - reject_if_identifier_matches
            - reject_always
            - update
            - update_if_identifier_matches
            - update_always
            - replace
            - replace_if_identifier_matches
            - replace_always
            - skip_if_contacted
            - skip_if_contacted_identifier_matches
            - skip_if_contacted_always
          description: >
            Acción a aplicar sobre el contacto duplicado: descartarlo
            (`reject`), combinar los datos

            nuevos con los existentes (`update`), sustituirlos (`replace`), o
            descartarlo solo si ya fue

            contactado (`skip_if_contacted`).


            Los sufijos delimitan cuándo se aplica la acción:
            `_if_identifier_matches` la restringe a los

            casos en que además coincide el `identifier`, y `_always` la aplica
            sin condiciones

            adicionales.
    BatchRules:
      type: object
      required:
        - strategy
      description: >
        Reglas de marcación del lote. Objeto completo: no admite actualizaciones
        parciales.
      properties:
        strategy:
          type: object
          description: >
            Límites de intentos y ventana horaria. Los rangos los valida el
            servicio; los valores

            fuera de rango responden `400`.
          properties:
            maxAttemptsPerAddress:
              type: integer
              description: Intentos máximos por cada teléfono o email del contacto.
            maxAttemptsPerContact:
              type: integer
              description: >
                Intentos máximos por contacto, sumando todas sus direcciones.
                Tiene que ser coherente

                con `maxAttemptsPerAddress`.
            timeBetweenAttempts:
              type: integer
              description: Minutos de espera entre intentos al mismo contacto.
            scheduleStart:
              type: string
              pattern: ^([01]\d|2[0-3]):([0-5]\d)$
              example: '09:00'
            scheduleEnd:
              type: string
              pattern: ^([01]\d|2[0-3]):([0-5]\d)$
              example: '20:00'
            timezone:
              type: string
              example: America/Argentina/Buenos_Aires
            daysOfWeek:
              type: array
              items:
                type: integer
                minimum: 0
                maximum: 6
              description: >
                Días habilitados, `0` domingo a `6` sábado. Vacío u omitido
                significa todos los días.
            processAllContactsFirst:
              type: boolean
              description: >
                Con `true`, se recorre la lista completa de contactos antes de
                reintentar con los que no

                atendieron. Con `false`, cada contacto se reintenta en cuanto
                vence su tiempo de espera.
        resultRules:
          type: object
          additionalProperties:
            type: object
            properties:
              retryDelay:
                type: integer
                description: Minutos a esperar antes de reintentar tras este resultado.
              maxRetries:
                type: integer
                description: |
                  Reintentos para este resultado. `0` significa no reintentar.
          description: >
            Reglas por resultado de la llamada. Entre las claves admitidas se
            encuentran `noAnswer`,

            `busy` y `voicemail`. Qué resultados aplican depende del tipo de
            canal del lote.
        addressPriority:
          type: object
          description: >-
            Define cómo se recorren las direcciones de un contacto antes de
            reintentarlas.
          properties:
            tryAllBeforeRetry:
              type: boolean
              description: >
                Con `true`, se prueban todas las direcciones del contacto antes
                de reintentar una ya

                utilizada.
        amd:
          type: object
          description: Detección de contestador automático.
          properties:
            amdEnabled:
              type: boolean
            amdAction:
              type: string
              enum:
                - HANGUP
                - LEAVE_MESSAGE
                - CONTINUE
    FieldRejection:
      type: object
      required:
        - index
        - field
        - code
        - reason
      description: >
        Campo rechazado de un contacto concreto.


        Se incluyen todos los rechazos, no solo el primero de cada contacto: un
        contacto con tres campos

        incorrectos genera tres entradas.


        Utilice `code`, que es estable, para tratar el error de forma
        programática. El campo `reason` es

        un texto descriptivo de apoyo y puede cambiar sin aviso.
      properties:
        index:
          type: integer
          description: Posición del contacto en el array enviado, base 0.
          example: 3
        field:
          type: string
          description: >
            Ruta del campo rechazado, precedida por la posición del contacto.
            Permite localizarlo en la

            petición enviada.
          example: contacts[3].addresses[0].address
        code:
          type: string
          enum:
            - REQUIRED
            - INVALID_TYPE
            - INVALID_PHONE_FORMAT
            - INVALID_EMAIL_FORMAT
            - INVALID_ADDRESS_TYPE
            - INVALID_PRIORITY
            - EMPTY_ARRAY
            - INVALID_DATE
            - DATE_NOT_FUTURE
            - DATE_OUT_OF_RANGE
            - DUPLICATED_IN_REQUEST
          description: >
            Código estable del motivo del rechazo. Es el valor recomendado para
            tratar el error de forma

            programática y para traducir el mensaje al idioma del usuario final.
        reason:
          type: string
          description: >
            Descripción del rechazo, en español. Informativa: puede cambiar sin
            aviso.
        meta:
          type: object
          additionalProperties: true
          description: >
            Datos del rechazo: el valor recibido, el esperado o el límite
            aplicado. Permiten construir un

            mensaje propio sin necesidad de interpretar el texto de `reason`.
          example:
            received: '123'
            expected: digits only, 7-15 characters
        error:
          type: string
          deprecated: true
          description: >
            Contiene el mismo valor que `reason`. Se mantiene por compatibilidad
            con versiones anteriores;

            utilice `reason`.
  responses:
    InternalError:
      description: Error interno.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/SimpleError'
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: apikey
      description: API key obtenida desde la interfaz de Inagent, dentro del marcador.

````